use tokio::io::AsyncWriteExt;
use crate::error::{Error, Result};
use crate::native::io::ClickHouseWrite;
use crate::tcp::client_info::ClientInfo;
use crate::tcp::protocol::{
ClientPacketId, DBMS_MIN_PROTOCOL_VERSION_WITH_ADDENDUM,
DBMS_MIN_PROTOCOL_VERSION_WITH_CHUNKED_PACKETS,
DBMS_MIN_PROTOCOL_VERSION_WITH_INTERSERVER_EXTERNALLY_GRANTED_ROLES,
DBMS_MIN_PROTOCOL_VERSION_WITH_PARAMETERS, DBMS_MIN_REVISION_WITH_BLOCK_INFO,
DBMS_MIN_REVISION_WITH_CLIENT_INFO, DBMS_MIN_REVISION_WITH_INTERSERVER_SECRET,
DBMS_MIN_REVISION_WITH_SETTINGS_SERIALIZED_AS_STRINGS, DBMS_MIN_REVISION_WITH_TEMPORARY_TABLES,
DBMS_MIN_REVISION_WITH_VERSIONED_PARALLEL_REPLICAS_PROTOCOL, DBMS_TCP_PROTOCOL_VERSION,
QueryProcessingStage,
};
pub(crate) const CLIENT_NAME: &str = "ClickHouse rust-client";
pub(crate) const CLIENT_VERSION_MAJOR_STR: &str = env!("CARGO_PKG_VERSION_MAJOR");
pub(crate) const CLIENT_VERSION_MINOR_STR: &str = env!("CARGO_PKG_VERSION_MINOR");
const SETTING_FLAG_CUSTOM: u64 = 2;
#[inline]
fn client_version_major() -> u64 {
CLIENT_VERSION_MAJOR_STR.parse().unwrap_or(0)
}
#[inline]
fn client_version_minor() -> u64 {
CLIENT_VERSION_MINOR_STR.parse().unwrap_or(0)
}
pub(crate) async fn send_hello<W: ClickHouseWrite>(
w: &mut W,
database: &str,
user: &str,
password: &str,
) -> Result<()> {
w.write_var_uint(ClientPacketId::Hello as u64).await?;
w.write_string(CLIENT_NAME.as_bytes()).await?;
w.write_var_uint(client_version_major()).await?;
w.write_var_uint(client_version_minor()).await?;
w.write_var_uint(DBMS_TCP_PROTOCOL_VERSION).await?;
w.write_string(database.as_bytes()).await?;
w.write_string(user.as_bytes()).await?;
w.write_string(password.as_bytes()).await?;
w.flush().await?;
Ok(())
}
pub(crate) async fn send_addendum<W: ClickHouseWrite>(
w: &mut W,
server_revision: u64,
quota_key: &str,
) -> Result<()> {
let effective = server_revision.min(DBMS_TCP_PROTOCOL_VERSION);
if effective >= DBMS_MIN_PROTOCOL_VERSION_WITH_ADDENDUM {
w.write_string(quota_key.as_bytes()).await?;
}
if effective >= DBMS_MIN_PROTOCOL_VERSION_WITH_CHUNKED_PACKETS {
w.write_string(b"notchunked").await?;
w.write_string(b"notchunked").await?;
}
if effective >= DBMS_MIN_REVISION_WITH_VERSIONED_PARALLEL_REPLICAS_PROTOCOL {
w.write_var_uint(0).await?;
}
if effective >= DBMS_MIN_PROTOCOL_VERSION_WITH_ADDENDUM {
w.flush().await?;
}
Ok(())
}
pub(crate) async fn send_query<W: ClickHouseWrite>(
w: &mut W,
server_revision: u64,
query_id: &str,
query: &str,
extra_settings: &[(String, String)],
params: &[(String, String)],
client_info: &ClientInfo,
) -> Result<()> {
w.write_var_uint(ClientPacketId::Query as u64).await?;
w.write_string(query_id.as_bytes()).await?;
if server_revision >= DBMS_MIN_REVISION_WITH_CLIENT_INFO {
client_info.write_to(w, server_revision).await?;
}
if server_revision >= DBMS_MIN_REVISION_WITH_SETTINGS_SERIALIZED_AS_STRINGS {
for (name, value) in extra_settings {
w.write_string(name.as_bytes()).await?;
w.write_var_uint(0).await?;
w.write_string(value.as_bytes()).await?;
}
} else if !extra_settings.is_empty() {
return Err(Error::Other(
"tcp: cannot send query settings to server older than 20.1.2.4 \
(revision 54429); upgrade the server or drop the settings"
.into(),
));
}
w.write_string(b"").await?;
if server_revision.min(DBMS_TCP_PROTOCOL_VERSION)
>= DBMS_MIN_PROTOCOL_VERSION_WITH_INTERSERVER_EXTERNALLY_GRANTED_ROLES
{
w.write_string(b"").await?;
}
if server_revision >= DBMS_MIN_REVISION_WITH_INTERSERVER_SECRET {
w.write_string(b"").await?;
}
w.write_var_uint(QueryProcessingStage::Complete as u64)
.await?;
w.write_var_uint(0).await?;
w.write_string(query.as_bytes()).await?;
if server_revision >= DBMS_MIN_PROTOCOL_VERSION_WITH_PARAMETERS {
for (name, value) in params {
w.write_string(name.as_bytes()).await?;
w.write_var_uint(SETTING_FLAG_CUSTOM).await?;
w.write_string(value.as_bytes()).await?;
}
w.write_string(b"").await?;
} else if !params.is_empty() {
return Err(Error::Other(
"tcp: server revision predates bound query parameters \
(revision 54459); upgrade the server or inline the values"
.into(),
));
}
w.flush().await?;
Ok(())
}
pub(crate) async fn send_data_block<W: ClickHouseWrite>(
w: &mut W,
server_revision: u64,
table_name: &str,
column_bytes: &[u8],
num_columns: u64,
num_rows: u64,
) -> Result<()> {
w.write_var_uint(ClientPacketId::Data as u64).await?;
if server_revision >= DBMS_MIN_REVISION_WITH_TEMPORARY_TABLES {
w.write_string(table_name.as_bytes()).await?;
}
if server_revision >= DBMS_MIN_REVISION_WITH_BLOCK_INFO {
w.write_var_uint(1).await?;
w.write_u8(0).await?; w.write_var_uint(2).await?;
w.write_i32_le(-1).await?; w.write_var_uint(0).await?; }
w.write_var_uint(num_columns).await?;
w.write_var_uint(num_rows).await?;
w.write_all(column_bytes).await?;
w.flush().await?;
Ok(())
}
pub(crate) async fn send_empty_block<W: ClickHouseWrite>(
w: &mut W,
server_revision: u64,
) -> Result<()> {
send_data_block(w, server_revision, "", &[], 0, 0).await
}
pub(crate) async fn send_cancel<W: ClickHouseWrite>(w: &mut W) -> Result<()> {
w.write_var_uint(ClientPacketId::Cancel as u64).await?;
w.flush().await?;
Ok(())
}
pub(crate) async fn send_ping<W: ClickHouseWrite>(w: &mut W) -> Result<()> {
w.write_var_uint(ClientPacketId::Ping as u64).await?;
w.flush().await?;
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::cast_possible_truncation)]
use super::*;
use crate::native::io::ClickHouseRead;
#[tokio::test]
async fn send_ping_writes_single_byte_varint() {
let mut buf = Vec::new();
send_ping(&mut buf).await.unwrap();
assert_eq!(buf, vec![4]);
}
#[tokio::test]
async fn send_cancel_writes_single_byte_varint() {
let mut buf = Vec::new();
send_cancel(&mut buf).await.unwrap();
assert_eq!(buf, vec![3]);
}
#[tokio::test]
async fn send_hello_byte_layout() {
let mut buf = Vec::new();
send_hello(&mut buf, "default", "user", "pw").await.unwrap();
assert_eq!(buf[0], 0);
let mut cur = std::io::Cursor::new(&buf[1..]);
let name = cur.read_utf8_string().await.unwrap();
assert_eq!(name, CLIENT_NAME);
let major = cur.read_var_uint().await.unwrap();
let minor = cur.read_var_uint().await.unwrap();
let revision = cur.read_var_uint().await.unwrap();
assert_eq!(major, client_version_major());
assert_eq!(minor, client_version_minor());
assert_eq!(revision, DBMS_TCP_PROTOCOL_VERSION);
assert_eq!(cur.read_utf8_string().await.unwrap(), "default");
assert_eq!(cur.read_utf8_string().await.unwrap(), "user");
assert_eq!(cur.read_utf8_string().await.unwrap(), "pw");
}
#[tokio::test]
async fn send_data_block_empty_layout() {
let mut buf = Vec::new();
send_empty_block(&mut buf, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
let mut cur = std::io::Cursor::new(&buf[..]);
assert_eq!(
cur.read_var_uint().await.unwrap(),
ClientPacketId::Data as u64
);
assert_eq!(cur.read_utf8_string().await.unwrap(), "");
assert_eq!(cur.read_var_uint().await.unwrap(), 1);
let mut b = [0u8; 1];
tokio::io::AsyncReadExt::read_exact(&mut cur, &mut b)
.await
.unwrap();
assert_eq!(b[0], 0);
assert_eq!(cur.read_var_uint().await.unwrap(), 2);
let bucket = tokio::io::AsyncReadExt::read_i32_le(&mut cur)
.await
.unwrap();
assert_eq!(bucket, -1);
assert_eq!(cur.read_var_uint().await.unwrap(), 0);
assert_eq!(cur.read_var_uint().await.unwrap(), 0);
assert_eq!(cur.read_var_uint().await.unwrap(), 0);
assert_eq!(cur.position() as usize, buf.len());
}
#[tokio::test]
async fn send_query_byte_layout_at_current_revision() {
let mut buf = Vec::new();
let ci = ClientInfo::for_initial_query(
CLIENT_NAME,
client_version_major(),
client_version_minor(),
DBMS_TCP_PROTOCOL_VERSION,
"",
);
let settings = vec![
("max_block_size".to_string(), "1024".to_string()),
("database".to_string(), "dfe".to_string()),
];
let params = vec![
("db".to_string(), "'default'".to_string()),
("n".to_string(), "42".to_string()),
];
send_query(
&mut buf,
DBMS_TCP_PROTOCOL_VERSION,
"qid",
"SELECT {n:UInt64}",
&settings,
¶ms,
&ci,
)
.await
.unwrap();
let mut cur = std::io::Cursor::new(&buf[..]);
assert_eq!(
cur.read_var_uint().await.unwrap(),
ClientPacketId::Query as u64
);
assert_eq!(cur.read_utf8_string().await.unwrap(), "qid");
skip_client_info(&mut cur).await;
for (name, value) in &settings {
assert_eq!(&cur.read_utf8_string().await.unwrap(), name);
assert_eq!(cur.read_var_uint().await.unwrap(), 0, "settings flags");
assert_eq!(&cur.read_utf8_string().await.unwrap(), value);
}
assert_eq!(cur.read_utf8_string().await.unwrap(), "");
assert_eq!(cur.read_utf8_string().await.unwrap(), "");
assert_eq!(cur.read_utf8_string().await.unwrap(), "");
assert_eq!(
cur.read_var_uint().await.unwrap(),
QueryProcessingStage::Complete as u64
);
assert_eq!(cur.read_var_uint().await.unwrap(), 0, "compression off");
assert_eq!(cur.read_utf8_string().await.unwrap(), "SELECT {n:UInt64}");
for (name, value) in ¶ms {
assert_eq!(&cur.read_utf8_string().await.unwrap(), name);
assert_eq!(
cur.read_var_uint().await.unwrap(),
SETTING_FLAG_CUSTOM,
"a bound parameter is a custom setting"
);
assert_eq!(&cur.read_utf8_string().await.unwrap(), value);
}
assert_eq!(cur.read_utf8_string().await.unwrap(), "");
assert_eq!(
cur.position() as usize,
buf.len(),
"the frame must be fully consumed"
);
}
async fn skip_client_info(cur: &mut std::io::Cursor<&[u8]>) {
use tokio::io::AsyncReadExt;
let mut one = [0u8; 1];
cur.read_exact(&mut one).await.unwrap(); for _ in 0..3 {
let _ = cur.read_utf8_string().await.unwrap(); }
let _ = cur.read_i64_le().await.unwrap(); cur.read_exact(&mut one).await.unwrap(); for _ in 0..3 {
let _ = cur.read_utf8_string().await.unwrap(); }
for _ in 0..3 {
let _ = cur.read_var_uint().await.unwrap(); }
let _ = cur.read_utf8_string().await.unwrap(); let _ = cur.read_var_uint().await.unwrap(); let _ = cur.read_var_uint().await.unwrap(); cur.read_exact(&mut one).await.unwrap(); for _ in 0..3 {
let _ = cur.read_var_uint().await.unwrap(); }
}
#[tokio::test]
async fn send_query_rejects_parameters_below_the_revision_gate() {
let mut buf = Vec::new();
let ci = ClientInfo::for_initial_query(
CLIENT_NAME,
client_version_major(),
client_version_minor(),
DBMS_TCP_PROTOCOL_VERSION,
"",
);
let params = vec![("db".to_string(), "'default'".to_string())];
let old = DBMS_MIN_PROTOCOL_VERSION_WITH_PARAMETERS - 1;
let err = send_query(
&mut buf,
old,
"qid",
"SELECT {db:String}",
&[],
¶ms,
&ci,
)
.await
.expect_err("parameters below the gate must be refused");
match err {
Error::Other(_) => {}
other => panic!("expected Error::Other, got {other:?}"),
}
}
#[tokio::test]
async fn send_query_rejects_old_revision() {
let mut buf = Vec::new();
let ci = ClientInfo::for_initial_query(
CLIENT_NAME,
client_version_major(),
client_version_minor(),
DBMS_TCP_PROTOCOL_VERSION,
"",
);
let old_revision = DBMS_MIN_REVISION_WITH_SETTINGS_SERIALIZED_AS_STRINGS - 1;
let settings = vec![("max_block_size".to_string(), "1024".to_string())];
let err = send_query(
&mut buf,
old_revision,
"qid",
"SELECT 1",
&settings,
&[],
&ci,
)
.await
.expect_err("expected an error for a too-old server revision");
let msg = err.to_string();
assert!(
msg.contains("54429") && msg.contains("query settings"),
"error must name the revision gate, got: {msg}"
);
}
}