use tokio::io::AsyncWriteExt;
use crate::error::Result;
use crate::native::io::ClickHouseWrite;
use crate::tcp::protocol::{
DBMS_MIN_PROTOCOL_VERSION_WITH_DISTRIBUTED_DEPTH,
DBMS_MIN_PROTOCOL_VERSION_WITH_INITIAL_QUERY_START_TIME,
DBMS_MIN_PROTOCOL_VERSION_WITH_PARALLEL_REPLICAS, DBMS_MIN_REVISION_WITH_OPENTELEMETRY,
DBMS_MIN_REVISION_WITH_QUOTA_KEY_IN_CLIENT_INFO, DBMS_MIN_REVISION_WITH_VERSION_PATCH,
};
pub(crate) const QUERY_KIND_INITIAL_QUERY: u8 = 1;
pub(crate) const INTERFACE_TYPE_TCP: u8 = 1;
#[derive(Debug, Clone, Default)]
pub(crate) struct ClientInfo {
pub(crate) query_kind: u8,
pub(crate) initial_user: String,
pub(crate) initial_query_id: String,
pub(crate) initial_address: String,
pub(crate) os_user: String,
pub(crate) client_hostname: String,
pub(crate) client_name: String,
pub(crate) client_version_major: u64,
pub(crate) client_version_minor: u64,
pub(crate) client_version_patch: u64,
pub(crate) client_revision: u64,
pub(crate) interface: u8,
pub(crate) quota_key: String,
pub(crate) distributed_depth: u64,
}
impl ClientInfo {
pub(crate) fn for_initial_query(
client_name: &str,
client_version_major: u64,
client_version_minor: u64,
client_revision: u64,
quota_key: &str,
) -> Self {
Self {
query_kind: QUERY_KIND_INITIAL_QUERY,
initial_user: String::new(),
initial_query_id: String::new(),
initial_address: "[::ffff:127.0.0.1]:0".to_string(),
os_user: String::new(),
client_hostname: String::new(),
client_name: client_name.to_string(),
client_version_major,
client_version_minor,
client_version_patch: 0,
client_revision,
interface: INTERFACE_TYPE_TCP,
quota_key: quota_key.to_string(),
distributed_depth: 0,
}
}
pub(crate) async fn write_to<W: ClickHouseWrite>(
&self,
w: &mut W,
server_revision: u64,
) -> Result<()> {
w.write_u8(self.query_kind).await?;
w.write_string(self.initial_user.as_bytes()).await?;
w.write_string(self.initial_query_id.as_bytes()).await?;
w.write_string(self.initial_address.as_bytes()).await?;
if server_revision >= DBMS_MIN_PROTOCOL_VERSION_WITH_INITIAL_QUERY_START_TIME {
w.write_i64_le(0).await?;
}
w.write_u8(self.interface).await?;
w.write_string(self.os_user.as_bytes()).await?;
w.write_string(self.client_hostname.as_bytes()).await?;
w.write_string(self.client_name.as_bytes()).await?;
w.write_var_uint(self.client_version_major).await?;
w.write_var_uint(self.client_version_minor).await?;
w.write_var_uint(self.client_revision).await?;
if server_revision >= DBMS_MIN_REVISION_WITH_QUOTA_KEY_IN_CLIENT_INFO {
w.write_string(self.quota_key.as_bytes()).await?;
}
if server_revision >= DBMS_MIN_PROTOCOL_VERSION_WITH_DISTRIBUTED_DEPTH {
w.write_var_uint(self.distributed_depth).await?;
}
if server_revision >= DBMS_MIN_REVISION_WITH_VERSION_PATCH {
w.write_var_uint(self.client_version_patch).await?;
}
if server_revision >= DBMS_MIN_REVISION_WITH_OPENTELEMETRY {
w.write_u8(0).await?;
}
if server_revision >= DBMS_MIN_PROTOCOL_VERSION_WITH_PARALLEL_REPLICAS {
w.write_var_uint(0).await?;
w.write_var_uint(0).await?;
w.write_var_uint(0).await?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::cast_possible_truncation)]
use super::*;
use crate::native::io::ClickHouseRead;
use crate::tcp::protocol::DBMS_TCP_PROTOCOL_VERSION;
use tokio::io::AsyncReadExt;
#[tokio::test]
async fn client_info_write_to_matches_cpp_field_order() {
let info = ClientInfo::for_initial_query("test-client", 25, 4, 54_459, "quota-1");
let mut buf = Vec::new();
info.write_to(&mut buf, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
let mut cur = std::io::Cursor::new(&buf[..]);
let mut one = [0u8; 1];
cur.read_exact(&mut one).await.unwrap();
assert_eq!(one[0], QUERY_KIND_INITIAL_QUERY, "query_kind");
assert_eq!(cur.read_utf8_string().await.unwrap(), "", "initial_user");
assert_eq!(
cur.read_utf8_string().await.unwrap(),
"",
"initial_query_id"
);
assert_eq!(
cur.read_utf8_string().await.unwrap(),
"[::ffff:127.0.0.1]:0",
"initial_address must parse as a Poco SocketAddress"
);
assert_eq!(
cur.read_i64_le().await.unwrap(),
0,
"initial_query_start_time_microseconds"
);
cur.read_exact(&mut one).await.unwrap();
assert_eq!(one[0], INTERFACE_TYPE_TCP, "iface_type");
assert_eq!(cur.read_utf8_string().await.unwrap(), "", "os_user");
assert_eq!(cur.read_utf8_string().await.unwrap(), "", "client_hostname");
assert_eq!(
cur.read_utf8_string().await.unwrap(),
"test-client",
"client_name"
);
assert_eq!(cur.read_var_uint().await.unwrap(), 25, "version_major");
assert_eq!(cur.read_var_uint().await.unwrap(), 4, "version_minor");
assert_eq!(
cur.read_var_uint().await.unwrap(),
54_459,
"client_revision"
);
assert_eq!(
cur.read_utf8_string().await.unwrap(),
"quota-1",
"quota_key"
);
assert_eq!(cur.read_var_uint().await.unwrap(), 0, "distributed_depth");
assert_eq!(cur.read_var_uint().await.unwrap(), 0, "version_patch");
cur.read_exact(&mut one).await.unwrap();
assert_eq!(one[0], 0, "opentelemetry absent marker");
for field in [
"collaborate_with_initiator",
"count_participating_replicas",
"number_of_current_replica",
] {
assert_eq!(cur.read_var_uint().await.unwrap(), 0, "{field}");
}
assert_eq!(
cur.position() as usize,
buf.len(),
"the block must be fully consumed"
);
}
#[tokio::test]
async fn quota_key_reaches_the_wire() {
let info = ClientInfo::for_initial_query("c", 1, 0, 54_459, "team-a");
let mut buf = Vec::new();
info.write_to(&mut buf, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
let needle = b"team-a";
assert!(
buf.windows(needle.len()).any(|w| w == needle),
"quota_key must appear in the ClientInfo block"
);
}
}