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}