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