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, SslMode, 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    /// # async fn example() -> fraiseql_wire::Result<()> {
22    /// use fraiseql_wire::FraiseClient;
23    ///
24    /// // TCP connection
25    /// let client = FraiseClient::connect("postgres://localhost/mydb").await?;
26    ///
27    /// // Unix socket
28    /// let client = FraiseClient::connect("postgres:///mydb").await?;
29    /// # Ok(())
30    /// # }
31    /// ```
32    pub async fn connect(connection_string: &str) -> Result<Self> {
33        let info = ConnectionInfo::parse(connection_string)?;
34        let tls_config = info.to_tls_config()?;
35
36        let transport = match info.transport {
37            TransportType::Tcp => {
38                let host = info.host.as_ref().expect("TCP requires host");
39                let port = info.port.expect("TCP requires port");
40                Transport::connect_tcp(host, port).await?
41            }
42            TransportType::Unix => {
43                let path = info.unix_socket.as_ref().expect("Unix requires path");
44                Transport::connect_unix(path).await?
45            }
46        };
47
48        let mut conn = Connection::new(transport);
49        let config = info.to_config();
50        let hostname = info.host.as_deref();
51        conn.startup(&config, tls_config.as_ref(), hostname).await?;
52
53        Ok(Self { conn })
54    }
55
56    /// Connect to Postgres with TLS encryption
57    ///
58    /// Uses the PostgreSQL SSLRequest protocol to negotiate TLS. The connection starts
59    /// as plain TCP, sends an SSLRequest message, and upgrades to TLS if the server
60    /// responds with `S`.
61    ///
62    /// # Examples
63    ///
64    /// ```no_run
65    /// # async fn example() -> fraiseql_wire::Result<()> {
66    /// use fraiseql_wire::{FraiseClient, connection::TlsConfig};
67    ///
68    /// // Configure TLS with system root certificates
69    /// let tls = TlsConfig::builder()
70    ///     .verify_hostname(true)
71    ///     .build()?;
72    ///
73    /// // Connect with TLS
74    /// let client = FraiseClient::connect_tls("postgres://secure.db.example.com/mydb", tls).await?;
75    /// # Ok(())
76    /// # }
77    /// ```
78    pub async fn connect_tls(
79        connection_string: &str,
80        tls_config: crate::connection::TlsConfig,
81    ) -> Result<Self> {
82        let info = ConnectionInfo::parse(connection_string)?;
83
84        match info.transport {
85            TransportType::Tcp => {
86                let host = info.host.as_ref().expect("TCP requires host");
87                let port = info.port.expect("TCP requires port");
88                // Start with plain TCP — SSLRequest negotiation upgrades to TLS
89                let transport = Transport::connect_tcp(host, port).await?;
90                let mut conn = Connection::new(transport);
91                let mut config = info.to_config();
92                config.sslmode = SslMode::Require;
93                conn.startup(&config, Some(&tls_config), Some(host)).await?;
94                Ok(Self { conn })
95            }
96            TransportType::Unix => Err(crate::Error::Config(
97                "TLS is only supported for TCP connections".into(),
98            )),
99        }
100    }
101
102    /// Connect to Postgres with custom connection configuration
103    ///
104    /// This method allows you to configure timeouts, keepalive intervals, and other
105    /// connection options. The connection configuration is merged with parameters from
106    /// the connection string.
107    ///
108    /// # Examples
109    ///
110    /// ```no_run
111    /// # async fn example() -> fraiseql_wire::Result<()> {
112    /// use fraiseql_wire::{FraiseClient, connection::ConnectionConfig};
113    /// use std::time::Duration;
114    ///
115    /// // Build connection configuration with timeouts
116    /// let config = ConnectionConfig::builder("localhost", "mydb")
117    ///     .password("secret")
118    ///     .statement_timeout(Duration::from_secs(30))
119    ///     .keepalive_idle(Duration::from_secs(300))
120    ///     .application_name("my_app")
121    ///     .build();
122    ///
123    /// // Connect with configuration
124    /// let client = FraiseClient::connect_with_config("postgres://localhost:5432/mydb", config).await?;
125    /// # Ok(())
126    /// # }
127    /// ```
128    pub async fn connect_with_config(
129        connection_string: &str,
130        config: ConnectionConfig,
131    ) -> Result<Self> {
132        let info = ConnectionInfo::parse(connection_string)?;
133        // Build TLS config from the ConnectionConfig's sslmode + connection string cert paths
134        let tls_config = info.to_tls_config()?;
135
136        let transport = match info.transport {
137            TransportType::Tcp => {
138                let host = info.host.as_ref().expect("TCP requires host");
139                let port = info.port.expect("TCP requires port");
140                Transport::connect_tcp(host, port).await?
141            }
142            TransportType::Unix => {
143                let path = info.unix_socket.as_ref().expect("Unix requires path");
144                Transport::connect_unix(path).await?
145            }
146        };
147
148        let mut conn = Connection::new(transport);
149        let hostname = info.host.as_deref();
150        conn.startup(&config, tls_config.as_ref(), hostname).await?;
151
152        Ok(Self { conn })
153    }
154
155    /// Connect to Postgres with both custom configuration and TLS encryption
156    ///
157    /// This method combines connection configuration (timeouts, keepalive, etc.)
158    /// with TLS encryption via the PostgreSQL SSLRequest protocol.
159    ///
160    /// # Examples
161    ///
162    /// ```no_run
163    /// # async fn example() -> fraiseql_wire::Result<()> {
164    /// use fraiseql_wire::{FraiseClient, connection::{ConnectionConfig, TlsConfig, SslMode}};
165    /// use std::time::Duration;
166    ///
167    /// // Configure connection with timeouts and TLS
168    /// let config = ConnectionConfig::builder("localhost", "mydb")
169    ///     .password("secret")
170    ///     .statement_timeout(Duration::from_secs(30))
171    ///     .sslmode(SslMode::VerifyFull)
172    ///     .build();
173    ///
174    /// // Configure TLS
175    /// let tls = TlsConfig::builder()
176    ///     .verify_hostname(true)
177    ///     .build()?;
178    ///
179    /// // Connect with both configuration and TLS
180    /// let client = FraiseClient::connect_with_config_and_tls(
181    ///     "postgres://secure.db.example.com/mydb",
182    ///     config,
183    ///     tls
184    /// ).await?;
185    /// # Ok(())
186    /// # }
187    /// ```
188    pub async fn connect_with_config_and_tls(
189        connection_string: &str,
190        config: ConnectionConfig,
191        tls_config: crate::connection::TlsConfig,
192    ) -> Result<Self> {
193        let info = ConnectionInfo::parse(connection_string)?;
194
195        match info.transport {
196            TransportType::Tcp => {
197                let host = info.host.as_ref().expect("TCP requires host");
198                let port = info.port.expect("TCP requires port");
199                // Start with plain TCP — SSLRequest negotiation upgrades to TLS
200                let transport = Transport::connect_tcp(host, port).await?;
201                let mut conn = Connection::new(transport);
202                conn.startup(&config, Some(&tls_config), Some(host)).await?;
203                Ok(Self { conn })
204            }
205            TransportType::Unix => Err(crate::Error::Config(
206                "TLS is only supported for TCP connections".into(),
207            )),
208        }
209    }
210
211    /// Start building a query for an entity with automatic deserialization
212    ///
213    /// The type parameter T controls consumer-side deserialization only.
214    /// Type T does NOT affect SQL generation, filtering, ordering, or wire protocol.
215    ///
216    /// # Examples
217    ///
218    /// Type-safe query (recommended):
219    /// ```no_run
220    /// # async fn example(client: fraiseql_wire::FraiseClient) -> fraiseql_wire::Result<()> {
221    /// use serde::Deserialize;
222    /// use futures::stream::StreamExt;
223    ///
224    /// #[derive(Deserialize)]
225    /// struct User {
226    ///     id: String,
227    ///     name: String,
228    /// }
229    ///
230    /// let mut stream = client
231    ///     .query::<User>("user")
232    ///     .where_sql("data->>'type' = 'customer'")  // SQL predicate
233    ///     .where_rust(|json| {
234    ///         // Rust predicate (applied client-side, on JSON)
235    ///         json["estimated_value"].as_f64().unwrap_or(0.0) > 1000.0
236    ///     })
237    ///     .order_by("data->>'name' ASC")
238    ///     .execute()
239    ///     .await?;
240    ///
241    /// while let Some(result) = stream.next().await {
242    ///     let user: User = result?;
243    ///     println!("User: {}", user.name);
244    /// }
245    /// # Ok(())
246    /// # }
247    /// ```
248    ///
249    /// Raw JSON query (debugging, forward compatibility):
250    /// ```no_run
251    /// # async fn example(client: fraiseql_wire::FraiseClient) -> fraiseql_wire::Result<()> {
252    /// use futures::stream::StreamExt;
253    ///
254    /// let mut stream = client
255    ///     .query::<serde_json::Value>("user")  // Escape hatch
256    ///     .execute()
257    ///     .await?;
258    ///
259    /// while let Some(result) = stream.next().await {
260    ///     let json = result?;
261    ///     println!("JSON: {:?}", json);
262    /// }
263    /// # Ok(())
264    /// # }
265    /// ```
266    pub fn query<T: DeserializeOwned + std::marker::Unpin + 'static>(
267        self,
268        entity: impl Into<String>,
269    ) -> QueryBuilder<T> {
270        QueryBuilder::new(self, entity)
271    }
272
273    /// Execute a raw SQL query (must match fraiseql-wire constraints)
274    pub(crate) async fn execute_query(
275        self,
276        sql: &str,
277        chunk_size: usize,
278        max_memory: Option<usize>,
279        soft_limit_warn_threshold: Option<f32>,
280        soft_limit_fail_threshold: Option<f32>,
281    ) -> Result<JsonStream> {
282        self.conn
283            .streaming_query(
284                sql,
285                chunk_size,
286                max_memory,
287                soft_limit_warn_threshold,
288                soft_limit_fail_threshold,
289                false, // enable_adaptive_chunking: disabled by default for backward compatibility
290                None,  // adaptive_min_chunk_size
291                None,  // adaptive_max_chunk_size
292            )
293            .await
294    }
295}