Skip to content

Commit 2eaaa70

Browse files
committed
Server side parameters via with_param
Fixes #142
1 parent de075b9 commit 2eaaa70

File tree

7 files changed

+264
-46
lines changed

7 files changed

+264
-46
lines changed

src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
#[macro_use]
66
extern crate static_assertions;
77

8-
use self::{error::Result, http_client::HttpClient};
8+
use self::{error::Result, http_client::HttpClient, sql::ser};
9+
use ::serde::Serialize;
910
use std::{collections::HashMap, fmt::Display, sync::Arc};
1011

1112
pub use self::{compression::Compression, row::Row};
@@ -160,6 +161,15 @@ impl Client {
160161
self
161162
}
162163

164+
/// Specify server side parameter for all this client's queries.
165+
///
166+
/// See also [`Query::with_param`].
167+
pub fn with_param(self, name: &str, value: impl Serialize) -> Result<Self, String> {
168+
let mut param = String::from("");
169+
ser::write_param(&mut param, &value)?;
170+
Ok(self.with_option(format!("param_{name}"), param))
171+
}
172+
163173
/// Used to specify a header that will be passed to all queries.
164174
///
165175
/// # Example

src/query.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use hyper::{header::CONTENT_LENGTH, Method, Request};
2-
use serde::Deserialize;
2+
use serde::{Deserialize, Serialize};
33
use std::fmt::Display;
44
use url::Url;
55

@@ -10,7 +10,7 @@ use crate::{
1010
request_body::RequestBody,
1111
response::Response,
1212
row::Row,
13-
sql::{Bind, SqlBuilder},
13+
sql::{ser, Bind, SqlBuilder},
1414
Client,
1515
};
1616

@@ -195,6 +195,19 @@ impl Query {
195195
self.client.add_option(name, value);
196196
self
197197
}
198+
199+
/// Specify server side parameter for query.
200+
///
201+
/// In queries you can reference params as {name: type} e.g. {val: Int32}.
202+
pub fn with_param(mut self, name: &str, value: impl Serialize) -> Self {
203+
let mut param = String::from("");
204+
if let Err(err) = ser::write_param(&mut param, &value) {
205+
self.sql = SqlBuilder::Failed(format!("invalid param: {err}"));
206+
self
207+
} else {
208+
self.with_option(format!("param_{name}"), param)
209+
}
210+
}
198211
}
199212

200213
/// A cursor that emits rows.

src/sql/bind.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ use super::{escape, ser};
88
#[sealed]
99
pub trait Bind {
1010
#[doc(hidden)]
11-
fn write(&self, dst: impl fmt::Write) -> Result<(), String>;
11+
fn write(&self, dst: &mut impl fmt::Write) -> Result<(), String>;
1212
}
1313

1414
#[sealed]
1515
impl<S: Serialize> Bind for S {
1616
#[inline]
17-
fn write(&self, mut dst: impl fmt::Write) -> Result<(), String> {
17+
fn write(&self, mut dst: &mut impl fmt::Write) -> Result<(), String> {
1818
ser::write_arg(&mut dst, self)
1919
}
2020
}
@@ -26,7 +26,7 @@ pub struct Identifier<'a>(pub &'a str);
2626
#[sealed]
2727
impl<'a> Bind for Identifier<'a> {
2828
#[inline]
29-
fn write(&self, dst: impl fmt::Write) -> Result<(), String> {
29+
fn write(&self, dst: &mut impl fmt::Write) -> Result<(), String> {
3030
escape::identifier(self.0, dst).map_err(|err| err.to_string())
3131
}
3232
}

src/sql/escape.rs

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,32 @@
11
use std::fmt;
22

3+
// Trust clickhouse-connect https://github.com/ClickHouse/clickhouse-connect/blob/5d85563410f3ec378cb199ec51d75e033211392c/clickhouse_connect/driver/binding.py#L15
4+
35
// See https://clickhouse.tech/docs/en/sql-reference/syntax/#syntax-string-literal
4-
pub(crate) fn string(src: &str, dst: impl fmt::Write) -> fmt::Result {
5-
escape(src, dst, '\'')
6+
pub(crate) fn string(src: &str, dst: &mut impl fmt::Write) -> fmt::Result {
7+
dst.write_char('\'')?;
8+
escape(src, dst)?;
9+
dst.write_char('\'')
610
}
711

812
// See https://clickhouse.tech/docs/en/sql-reference/syntax/#syntax-identifiers
9-
pub(crate) fn identifier(src: &str, dst: impl fmt::Write) -> fmt::Result {
10-
escape(src, dst, '`')
13+
pub(crate) fn identifier(src: &str, dst: &mut impl fmt::Write) -> fmt::Result {
14+
dst.write_char('\'')?;
15+
escape(src, dst)?;
16+
dst.write_char('\'')
1117
}
1218

13-
fn escape(src: &str, mut dst: impl fmt::Write, ch: char) -> fmt::Result {
14-
dst.write_char(ch)?;
15-
16-
// TODO: escape newlines?
17-
for (idx, part) in src.split(ch).enumerate() {
18-
if idx > 0 {
19-
dst.write_char('\\')?;
20-
dst.write_char(ch)?;
21-
}
22-
23-
for (idx, part) in part.split('\\').enumerate() {
24-
if idx > 0 {
25-
dst.write_str("\\\\")?;
26-
}
27-
28-
dst.write_str(part)?;
29-
}
19+
pub(crate) fn escape(src: &str, dst: &mut impl fmt::Write) -> fmt::Result {
20+
const REPLACE: &[char] = &['\\', '\'', '`', '\t', '\n'];
21+
let mut rest = src;
22+
while let Some(nextidx) = rest.find(REPLACE) {
23+
let (before, after) = rest.split_at(nextidx);
24+
rest = &after[1..];
25+
dst.write_str(before)?;
26+
dst.write_char('\\')?;
27+
dst.write_str(&after[..1])?;
3028
}
31-
32-
dst.write_char(ch)
29+
dst.write_str(rest)
3330
}
3431

3532
#[test]

src/sql/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub use bind::{Bind, Identifier};
99

1010
mod bind;
1111
pub(crate) mod escape;
12-
mod ser;
12+
pub(crate) mod ser;
1313

1414
#[derive(Debug, Clone)]
1515
pub(crate) enum SqlBuilder {

0 commit comments

Comments
 (0)