Skip to content

Commit 4b53bd0

Browse files
committed
Add query! macro providing a more ergonomic way to create parmeterized queries
1 parent f18c3e8 commit 4b53bd0

File tree

2 files changed

+184
-1
lines changed

2 files changed

+184
-1
lines changed

lib/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ pub use crate::errors::{
481481
Error, Neo4jClientErrorKind, Neo4jError, Neo4jErrorKind, Neo4jSecurityErrorKind, Result,
482482
};
483483
pub use crate::graph::{query, Graph};
484-
pub use crate::query::Query;
484+
pub use crate::query::{Query, QueryParameter};
485485
pub use crate::row::{Node, Path, Point2D, Point3D, Relation, Row, UnboundedRelation};
486486
pub use crate::stream::{DetachedRowStream, RowItem, RowStream};
487487
pub use crate::txn::Txn;

lib/src/query.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::cell::{Cell, RefCell};
2+
13
#[cfg(feature = "unstable-bolt-protocol-impl-v2")]
24
use crate::bolt::{Discard, Summary, WrapExtra as _};
35
use crate::{
@@ -26,6 +28,11 @@ impl Query {
2628
}
2729
}
2830

31+
pub fn with_params(mut self, params: BoltMap) -> Self {
32+
self.params = params;
33+
self
34+
}
35+
2936
pub fn param<T: Into<BoltType>>(mut self, key: &str, value: T) -> Self {
3037
self.params.put(key.into(), value.into());
3138
self
@@ -68,6 +75,14 @@ impl Query {
6875
self.extra.value.contains_key(key)
6976
}
7077

78+
pub fn query(&self) -> &str {
79+
&self.query
80+
}
81+
82+
pub fn get_params(&self) -> &BoltMap {
83+
&self.params
84+
}
85+
7186
pub(crate) async fn run(self, connection: &mut ManagedConnection) -> Result<()> {
7287
let request = BoltRequest::run(&self.query, self.params, self.extra);
7388
Self::try_run(request, connection)
@@ -170,6 +185,15 @@ impl From<&str> for Query {
170185
}
171186
}
172187

188+
impl std::fmt::Debug for Query {
189+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190+
f.debug_struct("Query")
191+
.field("query", &self.query)
192+
.field("params", &self.params)
193+
.finish_non_exhaustive()
194+
}
195+
}
196+
173197
type QueryResult<T> = Result<T, backoff::Error<Error>>;
174198

175199
fn wrap_error<T>(resp: impl IntoError, req: &'static str) -> QueryResult<T> {
@@ -217,6 +241,142 @@ fn unwrap_backoff(err: backoff::Error<Error>) -> Error {
217241
}
218242
}
219243

244+
#[doc(hidden)]
245+
pub struct QueryParameter<'x, T> {
246+
value: Cell<Option<T>>,
247+
name: &'static str,
248+
params: &'x RefCell<BoltMap>,
249+
}
250+
251+
impl<'x, T: Into<BoltType>> QueryParameter<'x, T> {
252+
#[allow(dead_code)]
253+
pub fn new(value: T, name: &'static str, params: &'x RefCell<BoltMap>) -> Self {
254+
Self {
255+
value: Cell::new(Some(value)),
256+
name,
257+
params,
258+
}
259+
}
260+
}
261+
262+
impl<T: Into<BoltType>> std::fmt::Display for QueryParameter<'_, T> {
263+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264+
let Some(v) = self.value.replace(None) else {
265+
return Err(std::fmt::Error);
266+
};
267+
self.params.borrow_mut().put(self.name.into(), v.into());
268+
write!(f, "${}", self.name)
269+
}
270+
}
271+
272+
impl<T: Into<BoltType>> std::fmt::Debug for QueryParameter<'_, T> {
273+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274+
std::fmt::Display::fmt(self, f)
275+
}
276+
}
277+
278+
/// Create a query with a format! like syntax
279+
///
280+
/// `query!` works similar to `format!`:
281+
/// - The first argument is the query string with `{<name>}` placeholders
282+
/// - Following that is a list of `name = value` parmeters arguments
283+
/// - All placeholders in the query strings are replaced with query parameters
284+
///
285+
/// The macro is a compiler-supported alternative to using the `params` method on `Query`.
286+
///
287+
/// ## Differences from `format!` and limitations
288+
///
289+
/// - Implicit `{name}` bindings without adding a `name = <value>` argument does not
290+
/// actually create a new parameter; It does default string interpolation instead.
291+
/// - Formatting parameters are largely ignored and have no effect on the query string.
292+
/// - Argument values need to implement `Into<BoltType>` instead of `Display`
293+
/// (and don't need to implement the latter)
294+
/// - Only named placeholders syntax is supported (`{<name>}` instead of `{}`)
295+
/// - This is because query parameters are always named
296+
/// - By extension, adding an unnamed argument (e.g. `<value>` instead of `name = <value>`) is also not supported
297+
///
298+
/// # Examples
299+
///
300+
/// ```
301+
/// use neo4rs::{query, Query};
302+
///
303+
/// // This creates an unparametrized query.
304+
/// let q: Query = query!("MATCH (n) RETURN n");
305+
/// assert_eq!(q.query(), "MATCH (n) RETURN n");
306+
/// assert!(q.get_params().is_empty());
307+
///
308+
/// // This creates a parametrized query.
309+
/// let q: Query = query!("MATCH (n) WHERE n.value = {answer} RETURN n", answer = 42);
310+
/// assert_eq!(q.query(), "MATCH (n) WHERE n.value = $answer RETURN n");
311+
/// assert_eq!(q.get_params().get::<i64>("answer").unwrap(), 42);
312+
///
313+
/// // by contrast, using the implicit string interpolation syntax does not
314+
/// // create a parameter, effectively being the same as `format!`.
315+
/// let answer = 42;
316+
/// let q: Query = query!("MATCH (n) WHERE n.value = {answer} RETURN n");
317+
/// assert_eq!(q.query(), "MATCH (n) WHERE n.value = 42 RETURN n");
318+
/// assert!(q.has_param_key("answer") == false);
319+
///
320+
/// // The value can be any type that implements Into<BoltType>, it does not
321+
/// // need to implement Display or Debug.
322+
/// use neo4rs::{BoltInteger, BoltType};
323+
///
324+
/// struct Answer;
325+
/// impl Into<BoltType> for Answer {
326+
/// fn into(self) -> BoltType {
327+
/// BoltType::Integer(BoltInteger::new(42))
328+
/// }
329+
/// }
330+
///
331+
/// let q: Query = query!("MATCH (n) WHERE n.value = {answer} RETURN n", answer = Answer);
332+
/// assert_eq!(q.query(), "MATCH (n) WHERE n.value = $answer RETURN n");
333+
/// assert_eq!(q.get_params().get::<i64>("answer").unwrap(), 42);
334+
/// ```
335+
#[macro_export]
336+
macro_rules! query {
337+
// Create a unparametrized query
338+
($query:expr) => {
339+
$crate::Query::new(format!($query))
340+
};
341+
342+
// Create a parametrized query with a format! like syntax
343+
($query:expr $(, $($input:tt)*)?) => {
344+
$crate::query!(@internal $query, [] $(; $($input)*)?)
345+
};
346+
347+
(@internal $query:expr, [$($acc:tt)*]; $name:ident = $value:expr $(, $($rest:tt)*)?) => {
348+
$crate::query!(@internal $query, [$($acc)* ($name = $value)] $(; $($rest)*)?)
349+
};
350+
351+
(@internal $query:expr, [$($acc:tt)*]; $value:expr $(, $($rest:tt)*)?) => {
352+
compile_error!("Only named parameter syntax (`name = value`) is supported");
353+
};
354+
355+
(@internal $query:expr, [$($acc:tt)*];) => {
356+
$crate::query!(@final $query; $($acc)*)
357+
};
358+
359+
(@internal $query:expr, [$($acc:tt)*]) => {
360+
$crate::query!(@final $query; $($acc)*)
361+
};
362+
363+
(@final $query:expr; $(($name:ident = $value:expr))*) => {{
364+
let params = $crate::BoltMap::default();
365+
let params = ::std::cell::RefCell::new(params);
366+
367+
let query = format!($query, $(
368+
$name = $crate::QueryParameter::new(
369+
$value,
370+
stringify!($name),
371+
&params,
372+
),
373+
)*);
374+
let params = params.into_inner();
375+
376+
$crate::Query::new(query).with_params(params)
377+
}};
378+
}
379+
220380
#[cfg(test)]
221381
mod tests {
222382
use super::*;
@@ -238,4 +398,27 @@ mod tests {
238398
assert!(q.has_param_key("name"));
239399
assert!(!q.has_param_key("country"));
240400
}
401+
402+
#[test]
403+
fn query_macro() {
404+
let q = query!(
405+
"MATCH (n) WHERE n.name = {name} AND n.age > {age} RETURN n",
406+
age = 42,
407+
name = "Frobniscante",
408+
);
409+
410+
assert_eq!(
411+
q.query.as_str(),
412+
"MATCH (n) WHERE n.name = $name AND n.age > $age RETURN n"
413+
);
414+
415+
assert_eq!(
416+
q.params.get::<String>("name").unwrap(),
417+
String::from("Frobniscante")
418+
);
419+
assert_eq!(q.params.get::<i64>("age").unwrap(), 42);
420+
421+
assert!(q.has_param_key("name"));
422+
assert!(!q.has_param_key("country"));
423+
}
241424
}

0 commit comments

Comments
 (0)