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}