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}