use tokio::io::AsyncReadExt;
use crate::error::{Error, Result};
use crate::native::decode::{DecodedBlock, decode_block};
use crate::native::io::ClickHouseRead;
use crate::tcp::protocol::{
DBMS_MIN_PROTOCOL_VERSION_WITH_CHUNKED_PACKETS,
DBMS_MIN_PROTOCOL_VERSION_WITH_PASSWORD_COMPLEXITY_RULES,
DBMS_MIN_PROTOCOL_VERSION_WITH_SERVER_QUERY_TIME_IN_PROGRESS,
DBMS_MIN_PROTOCOL_VERSION_WITH_TOTAL_BYTES_IN_PROGRESS, DBMS_MIN_REVISION_WITH_BLOCK_INFO,
DBMS_MIN_REVISION_WITH_CLIENT_WRITE_INFO, DBMS_MIN_REVISION_WITH_INTERSERVER_SECRET_V2,
DBMS_MIN_REVISION_WITH_ROWS_BEFORE_AGGREGATION, DBMS_MIN_REVISION_WITH_SERVER_DISPLAY_NAME,
DBMS_MIN_REVISION_WITH_SERVER_TIMEZONE, DBMS_MIN_REVISION_WITH_TEMPORARY_TABLES,
DBMS_MIN_REVISION_WITH_VERSION_PATCH,
DBMS_MIN_REVISION_WITH_VERSIONED_PARALLEL_REPLICAS_PROTOCOL, DBMS_TCP_PROTOCOL_VERSION,
Exception, ProfileInfo, Progress, ServerHello, ServerPacketId, TCP_EXCEPTION_STACK_TRACE_CAP,
TableColumns, truncate_on_char_boundary,
};
const MAX_BLOCK_COLUMNS: u64 = 16_384;
const MAX_PASSWORD_COMPLEXITY_RULES: u64 = 256;
pub(crate) const DBMS_MIN_REVISION_WITH_TOTAL_ROWS_IN_PROGRESS: u64 = 51554;
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum ServerPacket {
Data {
table_name: Option<String>,
num_columns: u64,
num_rows: u64,
columns: Vec<(String, String)>,
},
DataBlock(DecodedBlock),
Exception(Exception),
Progress(Progress),
ProfileInfo(ProfileInfo),
Pong,
EndOfStream,
Log,
TableColumns(TableColumns),
ProfileEvents,
TimezoneUpdate(String),
}
pub(crate) async fn read_hello<R: ClickHouseRead>(r: &mut R) -> Result<ServerHello> {
let packet_type = r.read_var_uint().await?;
let id = ServerPacketId::from_u64(packet_type)?;
match id {
ServerPacketId::Hello => {
let server_name = r.read_utf8_string().await?;
let major = r.read_var_uint().await?;
let minor = r.read_var_uint().await?;
let revision = r.read_var_uint().await?;
let effective = revision.min(DBMS_TCP_PROTOCOL_VERSION);
if effective >= DBMS_MIN_REVISION_WITH_VERSIONED_PARALLEL_REPLICAS_PROTOCOL {
let _parallel_replicas_version = r.read_var_uint().await?;
}
let timezone = if effective >= DBMS_MIN_REVISION_WITH_SERVER_TIMEZONE {
Some(r.read_utf8_string().await?)
} else {
None
};
let display_name = if effective >= DBMS_MIN_REVISION_WITH_SERVER_DISPLAY_NAME {
Some(r.read_utf8_string().await?)
} else {
None
};
let patch = if effective >= DBMS_MIN_REVISION_WITH_VERSION_PATCH {
r.read_var_uint().await?
} else {
0
};
if effective >= DBMS_MIN_PROTOCOL_VERSION_WITH_CHUNKED_PACKETS {
let _proto_send = r.read_utf8_string().await?;
let _proto_recv = r.read_utf8_string().await?;
}
if effective >= DBMS_MIN_PROTOCOL_VERSION_WITH_PASSWORD_COMPLEXITY_RULES {
let rules = r.read_var_uint().await?;
if rules > MAX_PASSWORD_COMPLEXITY_RULES {
return Err(Error::BadResponse(format!(
"tcp: server declared {rules} password-complexity rules, over the \
{MAX_PASSWORD_COMPLEXITY_RULES} cap"
)));
}
for _ in 0..rules {
let _pattern = r.read_utf8_string().await?;
let _message = r.read_utf8_string().await?;
}
}
if effective >= DBMS_MIN_REVISION_WITH_INTERSERVER_SECRET_V2 {
let mut nonce = [0u8; 8];
r.read_exact(&mut nonce).await?;
}
Ok(ServerHello {
server_name,
version: (major, minor, patch),
revision,
timezone,
display_name,
})
}
ServerPacketId::Exception => {
let exc = read_exception(r).await?;
Err(exc.into_error())
}
other => Err(Error::BadResponse(format!(
"tcp: unexpected packet during handshake: {other:?}"
))),
}
}
pub(crate) async fn read_exception<R: ClickHouseRead>(r: &mut R) -> Result<Exception> {
let code = r.read_i32_le().await?;
let name = r.read_utf8_string().await?;
let message = r.read_utf8_string().await?;
let mut stack_trace = r.read_utf8_string().await?;
if stack_trace.len() > TCP_EXCEPTION_STACK_TRACE_CAP {
tracing::warn!(
truncated_from = stack_trace.len(),
cap = TCP_EXCEPTION_STACK_TRACE_CAP,
"tcp: server exception stack_trace truncated"
);
truncate_on_char_boundary(&mut stack_trace, TCP_EXCEPTION_STACK_TRACE_CAP);
}
let _obsolete_has_nested = r.read_u8().await?;
Ok(Exception {
code,
name,
message,
stack_trace,
})
}
pub(crate) async fn read_progress<R: ClickHouseRead>(
r: &mut R,
server_revision: u64,
) -> Result<Progress> {
let effective = server_revision.min(DBMS_TCP_PROTOCOL_VERSION);
let rows_read = r.read_var_uint().await?;
let bytes_read = r.read_var_uint().await?;
let total_rows_to_read = if effective >= DBMS_MIN_REVISION_WITH_TOTAL_ROWS_IN_PROGRESS {
r.read_var_uint().await?
} else {
0
};
if effective >= DBMS_MIN_PROTOCOL_VERSION_WITH_TOTAL_BYTES_IN_PROGRESS {
let _total_bytes_to_read = r.read_var_uint().await?;
}
let (written_rows, written_bytes) = if effective >= DBMS_MIN_REVISION_WITH_CLIENT_WRITE_INFO {
let wr = r.read_var_uint().await?;
let wb = r.read_var_uint().await?;
(wr, wb)
} else {
(0, 0)
};
if effective >= DBMS_MIN_PROTOCOL_VERSION_WITH_SERVER_QUERY_TIME_IN_PROGRESS {
let _elapsed_ns = r.read_var_uint().await?;
}
Ok(Progress {
rows_read,
bytes_read,
total_rows_to_read,
written_rows,
written_bytes,
})
}
pub(crate) async fn read_profile_info<R: ClickHouseRead>(
r: &mut R,
server_revision: u64,
) -> Result<ProfileInfo> {
let rows = r.read_var_uint().await?;
let blocks = r.read_var_uint().await?;
let bytes = r.read_var_uint().await?;
let applied_limit = r.read_u8().await? != 0;
let rows_before_limit = r.read_var_uint().await?;
let _ = r.read_u8().await?;
if server_revision.min(DBMS_TCP_PROTOCOL_VERSION)
>= DBMS_MIN_REVISION_WITH_ROWS_BEFORE_AGGREGATION
{
let _applied_aggregation = r.read_u8().await?;
let _rows_before_aggregation = r.read_var_uint().await?;
}
Ok(ProfileInfo {
rows,
blocks,
bytes,
applied_limit,
rows_before_limit,
})
}
pub(crate) async fn read_table_columns<R: ClickHouseRead>(r: &mut R) -> Result<TableColumns> {
let external_table_name = r.read_utf8_string().await?;
let columns_definition = r.read_utf8_string().await?;
Ok(TableColumns {
external_table_name,
columns_definition,
})
}
async fn read_data_block_header<R: ClickHouseRead>(
r: &mut R,
server_revision: u64,
) -> Result<(Option<String>, u64, u64)> {
let table_name = if server_revision >= DBMS_MIN_REVISION_WITH_TEMPORARY_TABLES {
Some(r.read_utf8_string().await?)
} else {
None
};
if server_revision >= DBMS_MIN_REVISION_WITH_BLOCK_INFO {
crate::native::decode::read_block_info(r).await?;
}
let num_columns = r.read_var_uint().await?;
if num_columns > MAX_BLOCK_COLUMNS {
return Err(Error::BadResponse(format!(
"tcp: block header declares {num_columns} columns, above the \
{MAX_BLOCK_COLUMNS} cap"
)));
}
let num_rows = r.read_var_uint().await?;
Ok((table_name, num_columns, num_rows))
}
pub(crate) async fn read_empty_data_block_schema<R: ClickHouseRead>(
r: &mut R,
num_columns: u64,
server_revision: u64,
) -> Result<Vec<(String, String)>> {
let has_custom_ser = server_revision
>= crate::native::encode::DBMS_MIN_PROTOCOL_VERSION_WITH_CUSTOM_SERIALIZATION;
let capacity = usize::try_from(num_columns).unwrap_or(usize::MAX);
let mut out: Vec<(String, String)> = Vec::new();
out.try_reserve(capacity)
.map_err(|e| Error::BadResponse(format!("tcp: cannot reserve {capacity} columns: {e}")))?;
for _ in 0..num_columns {
let name = r.read_utf8_string().await?;
let type_name = r.read_utf8_string().await?;
if has_custom_ser {
let flag = r.read_u8().await?;
if flag != 0 {
return Err(Error::BadResponse(format!(
"tcp: column '{name}' uses custom serialization flag {flag} \
-- only normal (0) is supported"
)));
}
}
out.push((name, type_name));
}
Ok(out)
}
async fn consume_telemetry_block<R: ClickHouseRead>(r: &mut R, server_revision: u64) -> Result<()> {
let (_tag, num_columns, num_rows) = read_data_block_header(r, server_revision).await?;
if num_rows == 0 {
let _ = read_empty_data_block_schema(r, num_columns, server_revision).await?;
} else {
let _ = decode_block(r, num_columns, num_rows, server_revision).await?;
}
Ok(())
}
pub(crate) async fn read_packet<R: ClickHouseRead>(
r: &mut R,
server_revision: u64,
) -> Result<ServerPacket> {
let packet_type = r.read_var_uint().await?;
let id = ServerPacketId::from_u64(packet_type)?;
match id {
ServerPacketId::Data | ServerPacketId::Totals | ServerPacketId::Extremes => {
let (table_name, num_columns, num_rows) =
read_data_block_header(r, server_revision).await?;
if num_rows == 0 {
let columns = read_empty_data_block_schema(r, num_columns, server_revision).await?;
Ok(ServerPacket::Data {
table_name,
num_columns,
num_rows,
columns,
})
} else {
let block = decode_block(r, num_columns, num_rows, server_revision).await?;
Ok(ServerPacket::DataBlock(block))
}
}
ServerPacketId::Exception => Ok(ServerPacket::Exception(read_exception(r).await?)),
ServerPacketId::Progress => Ok(ServerPacket::Progress(
read_progress(r, server_revision).await?,
)),
ServerPacketId::ProfileInfo => Ok(ServerPacket::ProfileInfo(
read_profile_info(r, server_revision).await?,
)),
ServerPacketId::Pong => Ok(ServerPacket::Pong),
ServerPacketId::EndOfStream => Ok(ServerPacket::EndOfStream),
ServerPacketId::Log => {
consume_telemetry_block(r, server_revision).await?;
Ok(ServerPacket::Log)
}
ServerPacketId::TableColumns => {
Ok(ServerPacket::TableColumns(read_table_columns(r).await?))
}
ServerPacketId::ProfileEvents => {
consume_telemetry_block(r, server_revision).await?;
Ok(ServerPacket::ProfileEvents)
}
ServerPacketId::TimezoneUpdate => {
Ok(ServerPacket::TimezoneUpdate(r.read_utf8_string().await?))
}
other @ ServerPacketId::Hello => Err(Error::BadResponse(format!(
"tcp: unexpected server packet {other:?} mid-stream"
))),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::cast_possible_truncation)]
use super::*;
use crate::native::io::ClickHouseWrite;
use crate::tcp::protocol::DBMS_TCP_PROTOCOL_VERSION;
use std::io::Cursor;
use tokio::io::AsyncWriteExt;
#[tokio::test]
async fn hello_writer_reader_roundtrip() {
let mut buf = Vec::new();
buf.write_var_uint(ServerPacketId::Hello as u64)
.await
.unwrap();
buf.write_string("ClickHouse server".as_bytes())
.await
.unwrap();
buf.write_var_uint(25).await.unwrap();
buf.write_var_uint(4).await.unwrap();
buf.write_var_uint(DBMS_TCP_PROTOCOL_VERSION).await.unwrap();
buf.write_var_uint(0).await.unwrap();
buf.write_string("Etc/UTC".as_bytes()).await.unwrap();
buf.write_string("ch-01".as_bytes()).await.unwrap();
buf.write_var_uint(7).await.unwrap();
buf.write_string("notchunked".as_bytes()).await.unwrap();
buf.write_string("notchunked".as_bytes()).await.unwrap();
buf.write_var_uint(0).await.unwrap();
buf.write_all(&[0u8; 8]).await.unwrap();
let mut cur = Cursor::new(buf);
let hello = read_hello(&mut cur).await.unwrap();
assert_eq!(hello.server_name, "ClickHouse server");
assert_eq!(hello.version, (25, 4, 7));
assert_eq!(hello.revision, DBMS_TCP_PROTOCOL_VERSION);
assert_eq!(hello.timezone.as_deref(), Some("Etc/UTC"));
assert_eq!(hello.display_name.as_deref(), Some("ch-01"));
}
#[tokio::test]
async fn exception_truncates_stack_trace_at_cap() {
let big = "x".repeat(TCP_EXCEPTION_STACK_TRACE_CAP + 1024);
let mut buf = Vec::new();
buf.write_i32_le(100i32).await.unwrap();
buf.write_string("DB::Exception".as_bytes()).await.unwrap();
buf.write_string("msg".as_bytes()).await.unwrap();
buf.write_string(big.as_bytes()).await.unwrap();
buf.write_u8(0).await.unwrap();
let mut cur = Cursor::new(buf);
let exc = read_exception(&mut cur).await.unwrap();
assert_eq!(exc.code, 100);
assert_eq!(exc.name, "DB::Exception");
assert_eq!(exc.message, "msg");
assert_eq!(exc.stack_trace.len(), TCP_EXCEPTION_STACK_TRACE_CAP);
}
#[tokio::test]
async fn exception_stack_trace_truncation_is_char_boundary_safe() {
let big = "\u{20ac}".repeat(TCP_EXCEPTION_STACK_TRACE_CAP);
assert!(!big.is_char_boundary(TCP_EXCEPTION_STACK_TRACE_CAP));
let mut buf = Vec::new();
buf.write_i32_le(100i32).await.unwrap();
buf.write_string("DB::Exception".as_bytes()).await.unwrap();
buf.write_string("msg".as_bytes()).await.unwrap();
buf.write_string(big.as_bytes()).await.unwrap();
buf.write_u8(0).await.unwrap();
let mut cur = Cursor::new(buf);
let exc = read_exception(&mut cur).await.unwrap();
assert!(exc.stack_trace.len() <= TCP_EXCEPTION_STACK_TRACE_CAP);
assert!(exc.stack_trace.len() > TCP_EXCEPTION_STACK_TRACE_CAP - 3);
}
#[tokio::test]
async fn read_exception_ignores_the_obsolete_nested_flag() {
let mut buf = Vec::new();
buf.write_i32_le(60i32).await.unwrap();
buf.write_string(b"DB::Exception").await.unwrap();
buf.write_string(b"outer").await.unwrap();
buf.write_string(b"").await.unwrap();
buf.write_u8(1).await.unwrap(); buf.write_var_uint(ServerPacketId::EndOfStream as u64)
.await
.unwrap();
let mut cur = Cursor::new(buf);
let exc = read_exception(&mut cur)
.await
.expect("the obsolete flag must not change the frame shape");
assert_eq!(exc.code, 60);
assert_eq!(exc.message, "outer");
let trailing = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
assert!(matches!(trailing, ServerPacket::EndOfStream));
}
#[tokio::test]
async fn read_packet_rejects_an_absurd_column_count() {
let mut buf = Vec::new();
buf.write_var_uint(ServerPacketId::Data as u64)
.await
.unwrap();
buf.write_string(b"").await.unwrap();
buf.write_var_uint(1).await.unwrap();
buf.write_u8(0).await.unwrap();
buf.write_var_uint(2).await.unwrap();
buf.write_i32_le(-1).await.unwrap();
buf.write_var_uint(0).await.unwrap();
buf.write_var_uint(1 << 60).await.unwrap();
let mut cur = Cursor::new(buf);
let err = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.expect_err("an absurd column count must be refused, not allocated");
match err {
Error::BadResponse(msg) => assert!(msg.contains("above the"), "got {msg}"),
other => panic!("expected BadResponse, got {other:?}"),
}
}
#[tokio::test]
async fn read_empty_data_block_schema_reserves_fallibly() {
let mut cur = Cursor::new(Vec::new());
let err = read_empty_data_block_schema(&mut cur, 1 << 60, DBMS_TCP_PROTOCOL_VERSION)
.await
.expect_err("an absurd column count must not be reserved infallibly");
match err {
Error::BadResponse(msg) => assert!(msg.contains("cannot reserve"), "got {msg}"),
other => panic!("expected BadResponse, got {other:?}"),
}
}
#[tokio::test]
async fn read_empty_data_block_schema_rejects_custom_serialization() {
let mut buf = Vec::new();
buf.write_string(b"n").await.unwrap();
buf.write_string(b"UInt64").await.unwrap();
buf.write_u8(1).await.unwrap();
let mut cur = Cursor::new(buf);
let err = read_empty_data_block_schema(&mut cur, 1, DBMS_TCP_PROTOCOL_VERSION)
.await
.expect_err("a non-zero custom-serialization flag must be refused");
match err {
Error::BadResponse(msg) => {
assert!(msg.contains("custom serialization flag 1"), "got {msg}");
}
other => panic!("expected BadResponse, got {other:?}"),
}
}
struct Prng(u64);
impl Prng {
fn next_u64(&mut self) -> u64 {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
self.0.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn next_byte(&mut self) -> u8 {
(self.next_u64() >> 33) as u8
}
}
#[tokio::test]
async fn read_hello_survives_garbage_and_truncation() {
let mut good = Vec::new();
good.write_string(b"ClickHouse server").await.unwrap();
good.write_var_uint(25).await.unwrap();
good.write_var_uint(4).await.unwrap();
good.write_var_uint(DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
good.write_var_uint(0).await.unwrap();
good.write_string(b"Etc/UTC").await.unwrap();
good.write_string(b"ch-01").await.unwrap();
good.write_var_uint(7).await.unwrap();
good.write_string(b"notchunked").await.unwrap();
good.write_string(b"notchunked").await.unwrap();
good.write_var_uint(0).await.unwrap();
good.write_all(&[0u8; 8]).await.unwrap();
let mut rng = Prng(0x2545_F491_4F6C_DD1D);
for case in 0..4096u32 {
let mut bytes = good.clone();
match case % 4 {
0 => {
let cut = (rng.next_u64() as usize) % (bytes.len() + 1);
bytes.truncate(cut);
}
1 => {
let at = (rng.next_u64() as usize) % bytes.len();
bytes[at] = rng.next_byte();
}
2 => {
let at = bytes.len() - 9;
for b in bytes.iter_mut().skip(at).take(4) {
*b = rng.next_byte() | 0x80;
}
}
_ => {
let len = (rng.next_u64() as usize) % 96;
bytes = (0..len).map(|_| rng.next_byte()).collect();
}
}
let mut cur = Cursor::new(bytes);
let _ = read_hello(&mut cur).await;
}
}
#[tokio::test]
async fn read_packet_survives_garbage_and_truncation() {
let mut good = Vec::new();
good.write_var_uint(ServerPacketId::Data as u64)
.await
.unwrap();
good.write_string(b"").await.unwrap();
good.write_var_uint(1).await.unwrap();
good.write_u8(0).await.unwrap();
good.write_var_uint(2).await.unwrap();
good.write_i32_le(-1).await.unwrap();
good.write_var_uint(0).await.unwrap();
good.write_var_uint(2).await.unwrap();
good.write_var_uint(0).await.unwrap();
good.write_string(b"n").await.unwrap();
good.write_string(b"UInt64").await.unwrap();
good.write_u8(0).await.unwrap();
good.write_string(b"s").await.unwrap();
good.write_string(b"String").await.unwrap();
good.write_u8(0).await.unwrap();
let mut rng = Prng(0x5DEE_CE66_D1CE_B00D);
for case in 0..4096u32 {
let mut bytes = good.clone();
match case % 4 {
0 => {
let cut = (rng.next_u64() as usize) % (bytes.len() + 1);
bytes.truncate(cut);
}
1 => {
let at = (rng.next_u64() as usize) % bytes.len();
bytes[at] = rng.next_byte();
}
2 => {
let n = 1 + (rng.next_u64() as usize) % 12;
for b in bytes.iter_mut().take(n) {
*b = rng.next_byte();
}
}
_ => {
let len = (rng.next_u64() as usize) % 64;
bytes = (0..len).map(|_| rng.next_byte()).collect();
}
}
let mut cur = Cursor::new(bytes);
let _ = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION).await;
}
}
#[tokio::test]
async fn progress_revision_gating_legacy() {
let mut buf = Vec::new();
buf.write_var_uint(100).await.unwrap();
buf.write_var_uint(2048).await.unwrap();
let mut cur = Cursor::new(buf);
let legacy_revision = DBMS_MIN_REVISION_WITH_TOTAL_ROWS_IN_PROGRESS - 1;
let p = read_progress(&mut cur, legacy_revision).await.unwrap();
assert_eq!(p.rows_read, 100);
assert_eq!(p.bytes_read, 2048);
assert_eq!(p.total_rows_to_read, 0);
assert_eq!(p.written_rows, 0);
assert_eq!(p.written_bytes, 0);
}
#[tokio::test]
async fn progress_revision_gating_modern() {
let mut buf = Vec::new();
buf.write_var_uint(100).await.unwrap(); buf.write_var_uint(2048).await.unwrap(); buf.write_var_uint(10_000).await.unwrap(); buf.write_var_uint(65_536).await.unwrap(); buf.write_var_uint(50).await.unwrap(); buf.write_var_uint(1024).await.unwrap(); buf.write_var_uint(12_345).await.unwrap(); let mut cur = Cursor::new(buf);
let p = read_progress(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
assert_eq!(p.rows_read, 100);
assert_eq!(p.bytes_read, 2048);
assert_eq!(p.total_rows_to_read, 10_000);
assert_eq!(p.written_rows, 50);
assert_eq!(p.written_bytes, 1024);
}
#[tokio::test]
async fn profile_info_roundtrip() {
let mut buf = Vec::new();
buf.write_var_uint(1000).await.unwrap(); buf.write_var_uint(5).await.unwrap(); buf.write_var_uint(32768).await.unwrap(); buf.write_u8(1).await.unwrap(); buf.write_var_uint(500).await.unwrap(); buf.write_u8(0).await.unwrap(); buf.write_u8(1).await.unwrap(); buf.write_var_uint(250).await.unwrap(); let mut cur = Cursor::new(buf);
let pi = read_profile_info(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
assert_eq!(pi.rows, 1000);
assert_eq!(pi.blocks, 5);
assert_eq!(pi.bytes, 32768);
assert!(pi.applied_limit);
assert_eq!(pi.rows_before_limit, 500);
}
#[tokio::test]
async fn table_columns_roundtrip() {
let mut buf = Vec::new();
buf.write_string("".as_bytes()).await.unwrap();
buf.write_string(
"columns format version: 1\n2 columns:\n`a` Int32\n`b` String\n".as_bytes(),
)
.await
.unwrap();
let mut cur = Cursor::new(buf);
let tc = read_table_columns(&mut cur).await.unwrap();
assert_eq!(tc.external_table_name, "");
assert!(tc.columns_definition.starts_with("columns format version"));
}
#[tokio::test]
async fn read_packet_dispatches_pong() {
let mut buf = Vec::new();
buf.write_var_uint(ServerPacketId::Pong as u64)
.await
.unwrap();
let mut cur = Cursor::new(buf);
let pkt = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
assert!(matches!(pkt, ServerPacket::Pong));
}
#[tokio::test]
async fn read_packet_dispatches_end_of_stream() {
let mut buf = Vec::new();
buf.write_var_uint(ServerPacketId::EndOfStream as u64)
.await
.unwrap();
let mut cur = Cursor::new(buf);
let pkt = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
assert!(matches!(pkt, ServerPacket::EndOfStream));
}
#[tokio::test]
async fn read_packet_consumes_empty_data_block_schema() {
let mut buf = Vec::new();
buf.write_var_uint(ServerPacketId::Data as u64)
.await
.unwrap();
buf.write_string(b"").await.unwrap(); buf.write_var_uint(1).await.unwrap();
buf.write_u8(0).await.unwrap();
buf.write_var_uint(2).await.unwrap();
buf.write_i32_le(-1).await.unwrap();
buf.write_var_uint(0).await.unwrap();
buf.write_var_uint(2).await.unwrap(); buf.write_var_uint(0).await.unwrap(); buf.write_string(b"n").await.unwrap();
buf.write_string(b"UInt64").await.unwrap();
buf.write_u8(0).await.unwrap(); buf.write_string(b"s").await.unwrap();
buf.write_string(b"String").await.unwrap();
buf.write_u8(0).await.unwrap();
buf.write_var_uint(ServerPacketId::EndOfStream as u64)
.await
.unwrap();
let mut cur = Cursor::new(buf);
let pkt = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
match pkt {
ServerPacket::Data {
table_name,
num_columns,
num_rows,
columns,
} => {
assert_eq!(table_name.as_deref(), Some(""));
assert_eq!(num_columns, 2);
assert_eq!(num_rows, 0);
assert_eq!(
columns,
vec![
("n".to_string(), "UInt64".to_string()),
("s".to_string(), "String".to_string()),
]
);
}
other => panic!("expected Data, got {other:?}"),
}
let trailing = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
assert!(matches!(trailing, ServerPacket::EndOfStream));
}
#[tokio::test]
async fn read_packet_consumes_profile_events_block() {
let mut buf = Vec::new();
buf.write_var_uint(ServerPacketId::ProfileEvents as u64)
.await
.unwrap();
buf.write_string(b"host-01").await.unwrap(); buf.write_var_uint(1).await.unwrap();
buf.write_u8(0).await.unwrap();
buf.write_var_uint(2).await.unwrap();
buf.write_i32_le(-1).await.unwrap();
buf.write_var_uint(0).await.unwrap();
buf.write_var_uint(1).await.unwrap(); buf.write_var_uint(1).await.unwrap(); buf.write_string(b"value").await.unwrap(); buf.write_string(b"UInt64").await.unwrap(); buf.write_u8(0).await.unwrap(); buf.write_u64_le(42).await.unwrap(); buf.write_var_uint(ServerPacketId::EndOfStream as u64)
.await
.unwrap();
let mut cur = Cursor::new(buf);
let pkt = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
assert!(matches!(pkt, ServerPacket::ProfileEvents));
let trailing = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
assert!(matches!(trailing, ServerPacket::EndOfStream));
}
#[tokio::test]
async fn read_packet_consumes_log_block() {
let mut buf = Vec::new();
buf.write_var_uint(ServerPacketId::Log as u64)
.await
.unwrap();
buf.write_string(b"log-tag").await.unwrap(); buf.write_var_uint(1).await.unwrap();
buf.write_u8(0).await.unwrap();
buf.write_var_uint(2).await.unwrap();
buf.write_i32_le(-1).await.unwrap();
buf.write_var_uint(0).await.unwrap();
buf.write_var_uint(1).await.unwrap(); buf.write_var_uint(1).await.unwrap(); buf.write_string(b"text").await.unwrap(); buf.write_string(b"String").await.unwrap(); buf.write_u8(0).await.unwrap(); buf.write_string(b"hello from server").await.unwrap(); buf.write_var_uint(ServerPacketId::EndOfStream as u64)
.await
.unwrap();
let mut cur = Cursor::new(buf);
let pkt = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
assert!(matches!(pkt, ServerPacket::Log));
let trailing = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
assert!(matches!(trailing, ServerPacket::EndOfStream));
}
#[tokio::test]
async fn read_packet_decodes_totals_block_then_eos() {
let mut buf = Vec::new();
buf.write_var_uint(ServerPacketId::Totals as u64)
.await
.unwrap();
buf.write_string(b"").await.unwrap(); buf.write_var_uint(1).await.unwrap(); buf.write_u8(0).await.unwrap();
buf.write_var_uint(2).await.unwrap(); buf.write_i32_le(-1).await.unwrap();
buf.write_var_uint(0).await.unwrap(); buf.write_var_uint(1).await.unwrap(); buf.write_var_uint(1).await.unwrap(); buf.write_string(b"total").await.unwrap(); buf.write_string(b"UInt64").await.unwrap(); buf.write_u8(0).await.unwrap(); buf.write_u64_le(99).await.unwrap(); buf.write_var_uint(ServerPacketId::EndOfStream as u64)
.await
.unwrap();
let mut cur = Cursor::new(buf);
let pkt = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
match pkt {
ServerPacket::DataBlock(block) => {
assert_eq!(block.num_rows, 1);
match &block.columns[0] {
crate::native::decode::DecodedColumn::UInt64(v) => assert_eq!(v, &vec![99u64]),
other => panic!("expected UInt64 totals, got {other:?}"),
}
}
other => panic!("expected DataBlock for Totals, got {other:?}"),
}
let trailing = read_packet(&mut cur, DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
assert!(matches!(trailing, ServerPacket::EndOfStream));
}
}