Skip to main content

hyperdb_api_core/client/grpc/
params.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Query parameter support for gRPC queries.
5//!
6//! This module provides types and utilities for parameterized queries over gRPC.
7//! Parameters can be passed as JSON or Arrow IPC format.
8//!
9//! # Example
10//!
11//! ```no_run
12//! use hyperdb_api_core::client::grpc::{GrpcClient, GrpcConfig, QueryParameters, ParameterStyle};
13//!
14//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
15//! # let config = GrpcConfig::new("http://localhost:7484");
16//! let mut client = GrpcClient::connect(config).await?;
17//!
18//! // Using JSON parameters with $1, $2 style (use from_json_value for mixed types)
19//! let params = QueryParameters::from_json_value(&serde_json::json!([42, "hello"]))?;
20//! let result = client.execute_query_with_params(
21//!     "SELECT * FROM users WHERE id = $1 AND name = $2",
22//!     params,
23//!     ParameterStyle::DollarNumbered,
24//! ).await?;
25//!
26//! // Using JSON parameters with named style
27//! let params = QueryParameters::json_named()
28//!     .add("id", &42i64)?
29//!     .add("name", &"hello")?
30//!     .build();
31//! let result = client.execute_query_with_params(
32//!     "SELECT * FROM users WHERE id = :id AND name = :name",
33//!     params,
34//!     ParameterStyle::Named,
35//! ).await?;
36//! # Ok(())
37//! # }
38//! ```
39
40use bytes::Bytes;
41use serde::Serialize;
42use serde_json::Value as JsonValue;
43
44use super::proto::hyper_service::query_param::{ParameterStyle as ProtoParameterStyle, Parameters};
45use super::proto::hyper_service::{QueryParameterArrow, QueryParameterJson};
46
47/// Parameter style for SQL queries.
48///
49/// This determines how parameters are referenced in the SQL query string.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum ParameterStyle {
52    /// Use question marks: `SELECT * FROM users WHERE id = ?`
53    #[default]
54    QuestionMark,
55    /// Use dollar-numbered placeholders: `SELECT * FROM users WHERE id = $1`
56    DollarNumbered,
57    /// Use named parameters with colon: `SELECT * FROM users WHERE id = :id`
58    Named,
59}
60
61impl From<ParameterStyle> for i32 {
62    fn from(style: ParameterStyle) -> Self {
63        match style {
64            ParameterStyle::QuestionMark => ProtoParameterStyle::QuestionMark as i32,
65            ParameterStyle::DollarNumbered => ProtoParameterStyle::DollarNumbered as i32,
66            ParameterStyle::Named => ProtoParameterStyle::Named as i32,
67        }
68    }
69}
70
71impl From<ParameterStyle> for ProtoParameterStyle {
72    fn from(style: ParameterStyle) -> Self {
73        match style {
74            ParameterStyle::QuestionMark => ProtoParameterStyle::QuestionMark,
75            ParameterStyle::DollarNumbered => ProtoParameterStyle::DollarNumbered,
76            ParameterStyle::Named => ProtoParameterStyle::Named,
77        }
78    }
79}
80
81/// Query parameters for gRPC queries.
82///
83/// Parameters can be encoded as JSON or Arrow IPC format.
84/// JSON is simpler but Arrow is more efficient for large parameter sets.
85#[derive(Debug, Clone)]
86pub enum QueryParameters {
87    /// JSON-encoded parameters.
88    Json(String),
89    /// Arrow IPC-encoded parameters. Stored as `Bytes` so the parameter
90    /// payload can be handed to prost without a copy.
91    Arrow(Bytes),
92}
93
94impl QueryParameters {
95    /// Creates JSON parameters from a JSON string.
96    ///
97    /// # Example
98    ///
99    /// ```
100    /// use hyperdb_api_core::client::grpc::QueryParameters;
101    ///
102    /// // For positional parameters ($1, $2 or ?)
103    /// let params = QueryParameters::from_json_string("[42, \"hello\"]");
104    ///
105    /// // For named parameters (:id, :name)
106    /// let params = QueryParameters::from_json_string(r#"{"id": 42, "name": "hello"}"#);
107    /// ```
108    pub fn from_json_string(json: impl Into<String>) -> Self {
109        QueryParameters::Json(json.into())
110    }
111
112    /// Creates JSON parameters from a serializable value.
113    ///
114    /// # Example
115    ///
116    /// ```
117    /// use hyperdb_api_core::client::grpc::QueryParameters;
118    ///
119    /// let params = QueryParameters::from_json_value(&vec![42, 100, 200])?;
120    /// # Ok::<(), serde_json::Error>(())
121    /// ```
122    ///
123    /// # Errors
124    ///
125    /// Returns a [`serde_json::Error`] if `value` cannot be serialized
126    /// to JSON (for example, a type with a failing `Serialize` impl).
127    pub fn from_json_value<T: Serialize>(value: &T) -> Result<Self, serde_json::Error> {
128        let json = serde_json::to_string(value)?;
129        Ok(QueryParameters::Json(json))
130    }
131
132    /// Creates JSON parameters for positional placeholders ($1, $2 or ?).
133    ///
134    /// Values are serialized as a JSON array. All values must be the same type.
135    /// For mixed types, use [`from_json_value`](Self::from_json_value) with `serde_json::json!`.
136    ///
137    /// # Example
138    ///
139    /// ```
140    /// use hyperdb_api_core::client::grpc::QueryParameters;
141    ///
142    /// // Same-type parameters
143    /// let params = QueryParameters::json_positional(&[&42i64, &100i64])?;
144    ///
145    /// // Mixed types: use from_json_value with serde_json::json!
146    /// let params = QueryParameters::from_json_value(&serde_json::json!([42, "hello", true]))?;
147    /// # Ok::<(), serde_json::Error>(())
148    /// ```
149    ///
150    /// # Errors
151    ///
152    /// Returns a [`serde_json::Error`] if any element of `values` fails
153    /// to serialize to JSON.
154    pub fn json_positional<T: Serialize + ?Sized>(
155        values: &[&T],
156    ) -> Result<Self, serde_json::Error> {
157        let json_values: Vec<JsonValue> = values
158            .iter()
159            .map(serde_json::to_value)
160            .collect::<Result<_, _>>()?;
161        let json = serde_json::to_string(&json_values)?;
162        Ok(QueryParameters::Json(json))
163    }
164
165    /// Creates a builder for named JSON parameters (:name style).
166    ///
167    /// # Example
168    ///
169    /// ```
170    /// use hyperdb_api_core::client::grpc::QueryParameters;
171    ///
172    /// let params = QueryParameters::json_named()
173    ///     .add("id", &42i64)?
174    ///     .add("name", &"Alice")?
175    ///     .add("active", &true)?
176    ///     .build();
177    /// # Ok::<(), serde_json::Error>(())
178    /// ```
179    #[must_use]
180    pub fn json_named() -> JsonNamedParamsBuilder {
181        JsonNamedParamsBuilder::new()
182    }
183
184    /// Creates Arrow IPC parameters from raw bytes.
185    ///
186    /// The bytes should contain an Arrow IPC stream with schema and a single
187    /// record batch containing the parameter values.
188    ///
189    /// # Example
190    ///
191    /// ```ignore
192    /// use hyperdb_api_core::client::grpc::QueryParameters;
193    /// use arrow::array::{Int64Array, StringArray};
194    /// use arrow::datatypes::{DataType, Field, Schema};
195    /// use arrow::record_batch::RecordBatch;
196    /// use arrow::ipc::writer::StreamWriter;
197    /// use std::sync::Arc;
198    ///
199    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
200    /// // Create Arrow arrays for parameters
201    /// let id_array = Int64Array::from(vec![42]);
202    /// let name_array = StringArray::from(vec!["Alice"]);
203    ///
204    /// // Create schema and record batch
205    /// let schema = Arc::new(Schema::new(vec![
206    ///     Field::new("id", DataType::Int64, false),
207    ///     Field::new("name", DataType::Utf8, false),
208    /// ]));
209    /// let batch = RecordBatch::try_new(schema.clone(), vec![
210    ///     Arc::new(id_array),
211    ///     Arc::new(name_array),
212    /// ])?;
213    ///
214    /// // Serialize to IPC
215    /// let mut buf = Vec::new();
216    /// let mut writer = StreamWriter::try_new(&mut buf, &batch.schema())?;
217    /// writer.write(&batch)?;
218    /// writer.finish()?;
219    ///
220    /// let params = QueryParameters::from_arrow(buf);
221    /// # Ok(())
222    /// # }
223    /// ```
224    pub fn from_arrow(data: impl Into<Bytes>) -> Self {
225        QueryParameters::Arrow(data.into())
226    }
227
228    /// Converts to the proto Parameters type.
229    pub(crate) fn into_proto(self) -> Parameters {
230        match self {
231            QueryParameters::Json(json) => {
232                Parameters::JsonParameters(QueryParameterJson { data: json })
233            }
234            QueryParameters::Arrow(data) => {
235                Parameters::ArrowParameters(QueryParameterArrow { data })
236            }
237        }
238    }
239
240    /// Returns true if this is JSON-encoded parameters.
241    pub fn is_json(&self) -> bool {
242        matches!(self, QueryParameters::Json(_))
243    }
244
245    /// Returns true if this is Arrow-encoded parameters.
246    pub fn is_arrow(&self) -> bool {
247        matches!(self, QueryParameters::Arrow(_))
248    }
249}
250
251/// Builder for named JSON parameters.
252///
253/// # Example
254///
255/// ```
256/// use hyperdb_api_core::client::grpc::QueryParameters;
257///
258/// let params = QueryParameters::json_named()
259///     .add("user_id", &123)?
260///     .add("email", &"user@example.com")?
261///     .build();
262/// # Ok::<(), serde_json::Error>(())
263/// ```
264#[derive(Debug, Default)]
265pub struct JsonNamedParamsBuilder {
266    params: serde_json::Map<String, JsonValue>,
267}
268
269impl JsonNamedParamsBuilder {
270    /// Creates a new builder.
271    #[must_use]
272    pub fn new() -> Self {
273        Self::default()
274    }
275
276    /// Adds a named parameter.
277    ///
278    /// # Arguments
279    ///
280    /// * `name` - Parameter name (without the colon prefix)
281    /// * `value` - Parameter value (must be JSON-serializable)
282    ///
283    /// # Errors
284    ///
285    /// Returns a [`serde_json::Error`] if `value` cannot be serialized
286    /// to JSON.
287    pub fn add<T: Serialize>(
288        mut self,
289        name: impl Into<String>,
290        value: &T,
291    ) -> Result<Self, serde_json::Error> {
292        let json_value = serde_json::to_value(value)?;
293        self.params.insert(name.into(), json_value);
294        Ok(self)
295    }
296
297    #[must_use]
298    /// Adds a null parameter.
299    pub fn add_null(mut self, name: impl Into<String>) -> Self {
300        self.params.insert(name.into(), JsonValue::Null);
301        self
302    }
303
304    /// Builds the `QueryParameters`.
305    ///
306    /// # Panics
307    ///
308    /// Does not panic in practice. The `serde_json::to_string` call on a
309    /// `Map<String, Value>` is infallible for valid JSON trees —
310    /// serde_json only fails when a user-defined `Serialize` impl
311    /// returns an error, which cannot happen for the already-validated
312    /// `Value` payloads inserted via [`Self::add`] and [`Self::add_null`].
313    #[must_use]
314    pub fn build(self) -> QueryParameters {
315        let json = serde_json::to_string(&JsonValue::Object(self.params))
316            .expect("serializing Map<String, Value> never fails");
317        QueryParameters::Json(json)
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn test_json_positional_integers() {
327        let params = QueryParameters::json_positional(&[&42i64, &100i64, &200i64]).unwrap();
328        match params {
329            QueryParameters::Json(json) => {
330                assert_eq!(json, r"[42,100,200]");
331            }
332            QueryParameters::Arrow(_) => panic!("Expected JSON parameters"),
333        }
334    }
335
336    #[test]
337    fn test_json_positional_strings() {
338        let params = QueryParameters::json_positional(&[&"hello", &"world"]).unwrap();
339        match params {
340            QueryParameters::Json(json) => {
341                assert_eq!(json, r#"["hello","world"]"#);
342            }
343            QueryParameters::Arrow(_) => panic!("Expected JSON parameters"),
344        }
345    }
346
347    #[test]
348    fn test_json_positional_mixed_via_value() {
349        // For mixed types, use from_json_value with serde_json::json!
350        let values = serde_json::json!([42, "hello", true]);
351        let params = QueryParameters::from_json_value(&values).unwrap();
352        match params {
353            QueryParameters::Json(json) => {
354                assert_eq!(json, r#"[42,"hello",true]"#);
355            }
356            QueryParameters::Arrow(_) => panic!("Expected JSON parameters"),
357        }
358    }
359
360    #[test]
361    fn test_json_named() {
362        let params = QueryParameters::json_named()
363            .add("id", &42i64)
364            .unwrap()
365            .add("name", &"Alice")
366            .unwrap()
367            .build();
368        match params {
369            QueryParameters::Json(json) => {
370                let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
371                assert_eq!(parsed["id"], 42);
372                assert_eq!(parsed["name"], "Alice");
373            }
374            QueryParameters::Arrow(_) => panic!("Expected JSON parameters"),
375        }
376    }
377
378    #[test]
379    fn test_json_named_with_null() {
380        let params = QueryParameters::json_named()
381            .add("id", &42i64)
382            .unwrap()
383            .add_null("optional")
384            .build();
385        match params {
386            QueryParameters::Json(json) => {
387                let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
388                assert_eq!(parsed["id"], 42);
389                assert!(parsed["optional"].is_null());
390            }
391            QueryParameters::Arrow(_) => panic!("Expected JSON parameters"),
392        }
393    }
394
395    #[test]
396    fn test_from_json_string() {
397        let params = QueryParameters::from_json_string(r#"{"foo": "bar"}"#);
398        assert!(params.is_json());
399        assert!(!params.is_arrow());
400    }
401
402    #[test]
403    fn test_arrow_params() {
404        let params = QueryParameters::from_arrow(vec![1, 2, 3, 4]);
405        assert!(params.is_arrow());
406        assert!(!params.is_json());
407    }
408
409    #[test]
410    fn test_parameter_style_conversion() {
411        assert_eq!(i32::from(ParameterStyle::QuestionMark), 3);
412        assert_eq!(i32::from(ParameterStyle::DollarNumbered), 1);
413        assert_eq!(i32::from(ParameterStyle::Named), 2);
414    }
415}