Skip to main content

geode_client/
grpc.rs

1//! gRPC transport implementation for Geode.
2//!
3//! This module provides gRPC client functionality using the tonic-generated
4//! `GeodeServiceClient` from `crate::proto::geode_service_client`.
5
6use std::collections::HashMap;
7
8use tonic::Request;
9use tonic::transport::{Channel, Endpoint};
10
11use crate::client::{Column, Page};
12use crate::dsn::Dsn;
13use crate::error::{Error, Result};
14use crate::proto;
15use crate::proto::execution_response::Payload;
16use crate::proto::geode_service_client::GeodeServiceClient;
17use crate::types::Value;
18
19/// gRPC client for Geode.
20///
21/// Provides gRPC-based connection to the Geode database server using the
22/// tonic-generated service client.
23pub struct GrpcClient {
24    client: GeodeServiceClient<Channel>,
25    session_id: String,
26}
27
28impl GrpcClient {
29    /// Connect to a Geode server using gRPC.
30    ///
31    /// # Arguments
32    ///
33    /// * `dsn` - Parsed DSN with gRPC transport
34    ///
35    /// # Example
36    ///
37    /// ```no_run
38    /// use geode_client::dsn::Dsn;
39    /// use geode_client::grpc::GrpcClient;
40    ///
41    /// # async fn example() -> geode_client::Result<()> {
42    /// let dsn = Dsn::parse("grpc://localhost:50051")?;
43    /// let client = GrpcClient::connect(&dsn).await?;
44    /// # Ok(())
45    /// # }
46    /// ```
47    pub async fn connect(dsn: &Dsn) -> Result<Self> {
48        let addr = if dsn.tls_enabled() {
49            format!("https://{}", dsn.address())
50        } else {
51            format!("http://{}", dsn.address())
52        };
53
54        let endpoint = Endpoint::from_shared(addr.clone())
55            .map_err(|e| Error::connection(format!("Invalid endpoint: {}", e)))?;
56
57        // Configure TLS if needed
58        let endpoint = if dsn.tls_enabled() && dsn.skip_verify() {
59            // Skip TLS verification (insecure - for development only)
60            endpoint
61                .tls_config(tonic::transport::ClientTlsConfig::new().with_enabled_roots())
62                .map_err(|e| Error::tls(format!("TLS config error: {}", e)))?
63        } else {
64            endpoint
65        };
66
67        let channel = endpoint
68            .connect()
69            .await
70            .map_err(|e| Error::connection(format!("gRPC connection failed to {}: {}", addr, e)))?;
71
72        let grpc_client = GeodeServiceClient::new(channel);
73
74        let mut client = Self {
75            client: grpc_client,
76            session_id: String::new(),
77        };
78
79        // Perform handshake
80        client
81            .handshake(dsn.username(), dsn.password(), dsn.graph())
82            .await?;
83
84        Ok(client)
85    }
86
87    /// Perform authentication handshake.
88    async fn handshake(
89        &mut self,
90        username: Option<&str>,
91        password: Option<&str>,
92        graph: Option<&str>,
93    ) -> Result<()> {
94        let request = proto::HelloRequest {
95            username: username.unwrap_or("").to_string(),
96            password: password.unwrap_or("").to_string(),
97            tenant_id: None,
98            client_name: "geode-rust".to_string(),
99            client_version: crate::VERSION.to_string(),
100            wanted_conformance: "minimum".to_string(),
101            graph: graph.map(String::from),
102            role: None,
103        };
104
105        let response = self
106            .client
107            .handshake(Request::new(request))
108            .await
109            .map_err(|e| Error::connection(format!("Handshake failed: {}", e)))?;
110
111        let resp = response.into_inner();
112        if !resp.success {
113            return Err(Error::auth(resp.error_message));
114        }
115
116        self.session_id = resp.session_id;
117        Ok(())
118    }
119
120    /// Execute a GQL query.
121    pub async fn query(&mut self, gql: &str) -> Result<(Page, Option<String>)> {
122        self.query_with_params(gql, &HashMap::new()).await
123    }
124
125    /// Execute a GQL query with parameters.
126    pub async fn query_with_params(
127        &mut self,
128        gql: &str,
129        params: &HashMap<String, Value>,
130    ) -> Result<(Page, Option<String>)> {
131        let proto_params: Vec<proto::Param> = params
132            .iter()
133            .map(|(k, v)| proto::Param {
134                name: k.clone(),
135                value: Some(v.to_proto_value()),
136            })
137            .collect();
138
139        let request = proto::ExecuteRequest {
140            session_id: self.session_id.clone(),
141            query: gql.to_string(),
142            params: proto_params,
143        };
144
145        let response = self
146            .client
147            .execute(Request::new(request))
148            .await
149            .map_err(|e| Error::query(format!("Query execution failed: {}", e)))?;
150
151        // Process streaming response
152        let mut stream = response.into_inner();
153        let mut columns = Vec::new();
154        let mut rows = Vec::new();
155        let mut final_page = true;
156        let mut ordered = false;
157        let mut order_keys = Vec::new();
158
159        while let Some(exec_resp) = stream
160            .message()
161            .await
162            .map_err(|e| Error::query(format!("Failed to read response: {}", e)))?
163        {
164            if let Some(payload) = exec_resp.payload {
165                match payload {
166                    Payload::Schema(schema) => {
167                        columns = schema
168                            .columns
169                            .into_iter()
170                            .map(|c| Column {
171                                name: c.name,
172                                col_type: c.r#type,
173                            })
174                            .collect();
175                    }
176                    Payload::Page(page) => {
177                        for row in page.rows {
178                            let mut row_map = HashMap::new();
179                            for (i, col) in columns.iter().enumerate() {
180                                let value = if i < row.values.len() {
181                                    crate::convert::proto_to_value(&row.values[i])
182                                } else {
183                                    Value::null()
184                                };
185                                row_map.insert(col.name.clone(), value);
186                            }
187                            rows.push(row_map);
188                        }
189                        final_page = page.r#final;
190                        ordered = page.ordered;
191                        order_keys = page.order_keys;
192                    }
193                    Payload::Error(err) => {
194                        return Err(Error::Query {
195                            code: err.code,
196                            message: err.message,
197                        });
198                    }
199                    Payload::Metrics(_) | Payload::Heartbeat(_) => {
200                        // Informational payloads, continue
201                    }
202                    Payload::Explain(_) | Payload::Profile(_) => {
203                        // Plan/profile payloads, continue
204                    }
205                }
206            }
207        }
208
209        Ok((
210            Page {
211                columns,
212                rows,
213                ordered,
214                order_keys,
215                final_page,
216            },
217            None,
218        ))
219    }
220
221    /// Begin a transaction.
222    pub async fn begin(&mut self) -> Result<()> {
223        let request = proto::BeginRequest {
224            read_only: false,
225            session_id: self.session_id.clone(),
226        };
227
228        self.client
229            .begin(Request::new(request))
230            .await
231            .map_err(|e| Error::connection(format!("Begin transaction failed: {}", e)))?;
232
233        Ok(())
234    }
235
236    /// Commit a transaction.
237    pub async fn commit(&mut self) -> Result<()> {
238        let request = proto::CommitRequest {
239            session_id: self.session_id.clone(),
240        };
241
242        self.client
243            .commit(Request::new(request))
244            .await
245            .map_err(|e| Error::connection(format!("Commit failed: {}", e)))?;
246
247        Ok(())
248    }
249
250    /// Rollback a transaction.
251    pub async fn rollback(&mut self) -> Result<()> {
252        let request = proto::RollbackRequest {
253            session_id: self.session_id.clone(),
254        };
255
256        self.client
257            .rollback(Request::new(request))
258            .await
259            .map_err(|e| Error::connection(format!("Rollback failed: {}", e)))?;
260
261        Ok(())
262    }
263
264    /// Create a savepoint within the current transaction.
265    ///
266    /// Note: Savepoints are not yet supported via the gRPC service definition.
267    /// This method will return an error until the gRPC service is updated.
268    pub async fn savepoint(&mut self, _name: &str) -> Result<()> {
269        Err(Error::connection(
270            "savepoint is not yet supported via gRPC transport",
271        ))
272    }
273
274    /// Roll back to a previously created savepoint.
275    ///
276    /// Note: Rollback-to-savepoint is not yet supported via the gRPC service definition.
277    /// This method will return an error until the gRPC service is updated.
278    pub async fn rollback_to(&mut self, _name: &str) -> Result<()> {
279        Err(Error::connection(
280            "rollback_to is not yet supported via gRPC transport",
281        ))
282    }
283
284    /// Send a ping request.
285    pub async fn ping(&mut self) -> Result<bool> {
286        let response = self
287            .client
288            .ping(Request::new(proto::PingRequest {}))
289            .await
290            .map_err(|e| Error::connection(format!("Ping failed: {}", e)))?;
291
292        Ok(response.into_inner().ok)
293    }
294
295    /// Close the connection.
296    pub fn close(&mut self) -> Result<()> {
297        // gRPC channels are automatically closed when dropped
298        Ok(())
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use crate::proto;
305
306    #[test]
307    fn test_convert_proto_value_string() {
308        let proto_val = proto::Value {
309            kind: Some(proto::value::Kind::StringVal(proto::StringValue {
310                value: "hello".to_string(),
311                kind: 0,
312            })),
313        };
314        let val = crate::convert::proto_to_value(&proto_val);
315        assert_eq!(val.as_string().unwrap(), "hello");
316    }
317
318    #[test]
319    fn test_convert_proto_value_int() {
320        let proto_val = proto::Value {
321            kind: Some(proto::value::Kind::IntVal(proto::IntValue {
322                value: 42,
323                kind: 0,
324            })),
325        };
326        let val = crate::convert::proto_to_value(&proto_val);
327        assert_eq!(val.as_int().unwrap(), 42);
328    }
329
330    #[test]
331    fn test_convert_proto_value_bool() {
332        let proto_val = proto::Value {
333            kind: Some(proto::value::Kind::BoolVal(true)),
334        };
335        let val = crate::convert::proto_to_value(&proto_val);
336        assert!(val.as_bool().unwrap());
337    }
338
339    #[test]
340    fn test_convert_proto_value_null() {
341        let proto_val = proto::Value {
342            kind: Some(proto::value::Kind::NullVal(proto::NullValue {})),
343        };
344        let val = crate::convert::proto_to_value(&proto_val);
345        assert!(val.is_null());
346    }
347
348    #[test]
349    fn test_convert_proto_value_none() {
350        let proto_val = proto::Value { kind: None };
351        let val = crate::convert::proto_to_value(&proto_val);
352        assert!(val.is_null());
353    }
354
355    #[test]
356    fn test_convert_proto_value_double() {
357        let proto_val = proto::Value {
358            kind: Some(proto::value::Kind::DoubleVal(proto::DoubleValue {
359                value: 3.15,
360                kind: 0,
361            })),
362        };
363        let val = crate::convert::proto_to_value(&proto_val);
364        assert!(val.as_decimal().is_ok());
365    }
366
367    #[test]
368    fn test_convert_proto_value_list() {
369        let proto_val = proto::Value {
370            kind: Some(proto::value::Kind::ListVal(proto::ListValue {
371                values: vec![
372                    proto::Value {
373                        kind: Some(proto::value::Kind::IntVal(proto::IntValue {
374                            value: 1,
375                            kind: 0,
376                        })),
377                    },
378                    proto::Value {
379                        kind: Some(proto::value::Kind::IntVal(proto::IntValue {
380                            value: 2,
381                            kind: 0,
382                        })),
383                    },
384                ],
385            })),
386        };
387        let val = crate::convert::proto_to_value(&proto_val);
388        let arr = val.as_array().unwrap();
389        assert_eq!(arr.len(), 2);
390        assert_eq!(arr[0].as_int().unwrap(), 1);
391        assert_eq!(arr[1].as_int().unwrap(), 2);
392    }
393
394    #[test]
395    fn test_convert_proto_value_map() {
396        let proto_val = proto::Value {
397            kind: Some(proto::value::Kind::MapVal(proto::MapValue {
398                entries: vec![proto::MapEntry {
399                    key: "name".to_string(),
400                    value: Some(proto::Value {
401                        kind: Some(proto::value::Kind::StringVal(proto::StringValue {
402                            value: "Alice".to_string(),
403                            kind: 0,
404                        })),
405                    }),
406                }],
407            })),
408        };
409        let val = crate::convert::proto_to_value(&proto_val);
410        let obj = val.as_object().unwrap();
411        assert_eq!(obj.get("name").unwrap().as_string().unwrap(), "Alice");
412    }
413}