Skip to content

Commit faded14

Browse files
authored
Add query! macro providing a more ergonomic way to create parmeterized queries (#214)
* Add query! macro providing a more ergonomic way to create parmeterized queries * Change examples/tests to use query macro where possible
1 parent f18c3e8 commit faded14

File tree

5 files changed

+209
-21
lines changed

5 files changed

+209
-21
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
}

lib/tests/missing_properties.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ async fn missing_properties() {
1010

1111
let a_val = None::<String>;
1212
let mut result = graph
13-
.execute(query("CREATE (ts:TestStruct {a: $a}) RETURN ts").param("a", a_val))
13+
.execute(query!(
14+
"CREATE (ts:TestStruct {{a: {a}}}) RETURN ts",
15+
a = a_val
16+
))
1417
.await
1518
.unwrap();
1619
let row = result.next().await.unwrap().unwrap();

lib/tests/txn_change_db.rs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use futures::TryStreamExt;
2-
use neo4rs::*;
2+
use neo4rs::query;
33
use serde::Deserialize;
44

55
mod container;
@@ -19,7 +19,7 @@ async fn txn_changes_db() {
1919
return;
2020
}
2121

22-
std::panic::panic_any(e);
22+
std::panic::panic_any(e.to_string());
2323
}
2424
};
2525
let graph = neo4j.graph();
@@ -46,18 +46,13 @@ async fn txn_changes_db() {
4646

4747
let mut txn = graph.start_txn().await.unwrap();
4848
let mut databases = txn
49-
.execute(
50-
query(&format!(
51-
concat!(
52-
"SHOW TRANSACTIONS YIELD * WHERE username = $username AND currentQuery ",
53-
"STARTS WITH $query AND toLower({status_field}) = $status RETURN database"
54-
),
55-
status_field = status_field
56-
))
57-
.param("username", "neo4j")
58-
.param("query", "SHOW TRANSACTIONS YIELD ")
59-
.param("status", "running"),
60-
)
49+
.execute(query!(
50+
"SHOW TRANSACTIONS YIELD * WHERE username = {username} AND currentQuery
51+
STARTS WITH {query} AND toLower({status_field}) = {status} RETURN database",
52+
username = "neo4j",
53+
query = "SHOW TRANSACTIONS YIELD ",
54+
status = "running",
55+
))
6156
.await
6257
.unwrap();
6358

lib/tests/use_default_db.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,21 @@ async fn use_default_db() {
5454

5555
let id = uuid::Uuid::new_v4();
5656
graph
57-
.run(query("CREATE (:Node { uuid: $uuid })").param("uuid", id.to_string()))
57+
.run(query!(
58+
"CREATE (:Node {{ uuid: {uuid} }})",
59+
uuid = id.to_string()
60+
))
5861
.await
5962
.unwrap();
6063

6164
#[cfg(feature = "unstable-bolt-protocol-impl-v2")]
6265
let query_stream = graph
6366
.execute_on(
6467
dbname.as_str(),
65-
query("MATCH (n:Node {uuid: $uuid}) RETURN count(n) AS result")
66-
.param("uuid", id.to_string()),
68+
query!(
69+
"MATCH (n:Node {{uuid: {uuid}}}) RETURN count(n) AS result",
70+
uuid = id.to_string()
71+
),
6772
Operation::Read,
6873
)
6974
.await;
@@ -72,8 +77,10 @@ async fn use_default_db() {
7277
let query_stream = graph
7378
.execute_on(
7479
dbname.as_str(),
75-
query("MATCH (n:Node {uuid: $uuid}) RETURN count(n) AS result")
76-
.param("uuid", id.to_string()),
80+
query!(
81+
"MATCH (n:Node {{uuid: {uuid}}}) RETURN count(n) AS result",
82+
uuid = id.to_string()
83+
),
7784
)
7885
.await;
7986

0 commit comments

Comments
 (0)