#![cfg(all(feature = "tcp", feature = "unified", feature = "dynamic"))]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::path::Path;
use std::time::Duration;
use testcontainers::core::{IntoContainerPort, WaitFor};
use testcontainers::runners::AsyncRunner;
use testcontainers::{ContainerAsync, GenericImage, ImageExt};
use tokio::sync::Semaphore;
use clickhouse::Client;
use clickhouse_dfe::native::DecodedBlock;
use clickhouse_dfe::{Columns, TcpClient, UnifiedClient};
const IMAGE_TAG: &str = "26.3.21.7";
const CONTAINER_MEMORY_BYTES: i64 = 2 * 1024 * 1024 * 1024;
static ONE_SERVER: Semaphore = Semaphore::const_new(1);
const DOCKER_SOCKETS: &[&str] = &["/var/run/docker.sock", "/run/docker.sock"];
fn docker_endpoint_exists() -> bool {
if std::env::var_os("DOCKER_HOST").is_some() {
return true;
}
if DOCKER_SOCKETS.iter().any(|s| Path::new(s).exists()) {
return true;
}
std::env::var_os("XDG_RUNTIME_DIR").is_some_and(|dir| {
let dir = Path::new(&dir);
dir.join("docker.sock").exists() || dir.join("podman/podman.sock").exists()
})
}
macro_rules! require_docker {
() => {
if !docker_endpoint_exists() {
assert!(
std::env::var_os("CI").is_none(),
"CI runs the wire suite: no Docker endpoint (DOCKER_HOST unset, no socket)"
);
eprintln!("skipped: no Docker endpoint -- start dockerd or set DOCKER_HOST");
return;
}
};
}
struct Server {
container: ContainerAsync<GenericImage>,
native_port: u16,
http_port: u16,
}
async fn server() -> Server {
let container = GenericImage::new("clickhouse/clickhouse-server", IMAGE_TAG)
.with_exposed_port(9000.tcp())
.with_exposed_port(8123.tcp())
.with_wait_for(WaitFor::Nothing)
.with_env_var("CLICKHOUSE_SKIP_USER_SETUP", "1")
.with_host_config_modifier(|host| host.memory = Some(CONTAINER_MEMORY_BYTES))
.with_startup_timeout(Duration::from_secs(120))
.start()
.await
.expect("the pinned ClickHouse image starts");
let native_port = container.get_host_port_ipv4(9000).await.unwrap();
let http_port = container.get_host_port_ipv4(8123).await.unwrap();
let server = Server {
container,
native_port,
http_port,
};
await_ready(&server).await;
server
}
impl Server {
fn tcp(&self) -> UnifiedClient {
UnifiedClient::Tcp(TcpClient::new(format!("127.0.0.1:{}", self.native_port)))
}
fn http(&self) -> UnifiedClient {
UnifiedClient::Http(
Client::default().with_url(format!("http://127.0.0.1:{}", self.http_port)),
)
}
async fn stop(self) {
let _ = self.container.rm().await;
}
}
async fn await_ready(server: &Server) {
let http = UnifiedClient::Http(
Client::default().with_url(format!("http://127.0.0.1:{}", server.http_port)),
);
let tcp = UnifiedClient::Tcp(TcpClient::new(format!("127.0.0.1:{}", server.native_port)));
let deadline = tokio::time::Instant::now() + Duration::from_secs(60);
let mut last = String::from("never attempted");
while tokio::time::Instant::now() < deadline {
match (http.ping().await, tcp.ping().await) {
(Ok(()), Ok(())) => return,
(Err(e), _) | (_, Err(e)) => last = e.to_string(),
}
tokio::time::sleep(Duration::from_millis(250)).await;
}
panic!("the server never answered on both ports; last error: {last}");
}
struct Case {
ch_type: &'static str,
values: &'static [&'static str],
known_broken: Option<&'static str>,
}
const fn case(ch_type: &'static str, values: &'static [&'static str]) -> Case {
Case {
ch_type,
values,
known_broken: None,
}
}
const CASES: &[Case] = &[
case("UInt8", &["0", "255"]),
case("UInt16", &["0", "65535"]),
case("UInt32", &["0", "4294967295"]),
case("UInt64", &["0", "18446744073709551615"]),
case("UInt128", &["0", "340282366920938463463374607431768211455"]),
case("UInt256", &["0", "12345678901234567890"]),
case("Int8", &["-128", "127"]),
case("Int16", &["-32768", "32767"]),
case("Int32", &["-2147483648", "2147483647"]),
case("Int64", &["-9223372036854775808", "9223372036854775807"]),
case("Int128", &["-170141183460469231731687303715884105728", "0"]),
case("Int256", &["-1", "1"]),
case("Float32", &["-1.5", "3.25"]),
case("Float64", &["-1.5", "3.25"]),
case("BFloat16", &["1.5", "-2"]),
case("Bool", &["true", "false"]),
case("Decimal(9, 2)", &["'-1234567.89'", "'0.01'"]),
case("Decimal(18, 4)", &["'-1.0001'", "'12345678901.2345'"]),
case("Decimal(38, 10)", &["'-1.0000000001'", "'0'"]),
case("Decimal(76, 20)", &["'-1.00000000000000000001'", "'0'"]),
case("Date", &["'1970-01-01'", "'2149-06-06'"]),
case("Date32", &["'1900-01-01'", "'2299-12-31'"]),
case(
"DateTime",
&["'1970-01-01 00:00:00'", "'2106-02-07 06:28:15'"],
),
case(
"DateTime('Australia/Sydney')",
&["'2026-09-02 08:00:00'", "'1970-01-01 10:00:00'"],
),
case(
"DateTime64(3, 'UTC')",
&["'2026-09-02 08:00:00.123'", "'1970-01-01 00:00:00.000'"],
),
case("DateTime64(9)", &["'2026-09-02 08:00:00.123456789'"]),
case(
"UUID",
&["'00000000-0000-0000-0000-000000000000'", "generateUUIDv4()"],
),
case("IPv4", &["'127.0.0.1'", "'255.255.255.255'"]),
case("IPv6", &["'::1'", "'2001:db8::1'"]),
case("Enum8('a' = 1, 'b' = -2)", &["'a'", "'b'"]),
case("Enum16('a' = 1, 'b' = -300)", &["'a'", "'b'"]),
case("String", &["''", "'ünïcode'", "concat('a', char(0), 'b')"]),
case("FixedString(4)", &["'ab'", "'abcd'"]),
case("Nullable(UInt64)", &["NULL", "7"]),
case("Nullable(String)", &["NULL", "'x'"]),
case(
"Nullable(DateTime64(3))",
&["NULL", "'2026-09-02 08:00:00.500'"],
),
case("LowCardinality(String)", &["'a'", "'b'", "'a'"]),
case("LowCardinality(Nullable(String))", &["NULL", "'a'", "'a'"]),
case("Array(Int32)", &["[]", "[-1, 0, 1]"]),
case("Array(Nullable(UInt8))", &["[NULL, 1]", "[]"]),
case("Array(Array(String))", &["[]", "[['a'], [], ['b', 'c']]"]),
case("Array(LowCardinality(String))", &["[]", "['a', 'b', 'a']"]),
case("Map(String, Int64)", &["map()", "map('a', 1, 'b', -2)"]),
case("Map(String, Array(String))", &["map('a', ['x', 'y'])"]),
case(
"Map(String, LowCardinality(Nullable(String)))",
&["map('a', NULL, 'b', 'v')"],
),
case("Tuple(UInt8, String, Array(Int32))", &["(1, 'a', [1, 2])"]),
case(
"Array(Tuple(String, Map(String, UInt8)))",
&["[('a', map('k', 1))]", "[]"],
),
case("Point", &["(1.5, -2.5)"]),
case("SimpleAggregateFunction(sum, UInt64)", &["1", "2"]),
case("JSON", &[r#"'{"a":1,"b":{"c":"x"}}'"#, "'{}'"]),
case("JSON", &[r#"'{"arr":[1,2,3]}'"#]),
case(
"Variant(UInt64, String)",
&["'text'::String", "42::UInt64", "NULL"],
),
case("Dynamic", &["'text'", "42::UInt64", "NULL"]),
case("Tuple(LowCardinality(String), UInt8)", &["('a', 1)"]),
case("Array(JSON)", &[r#"['{"a":1}']"#, "[]"]),
case("Array(Dynamic)", &["['x', 42::UInt64]", "[]"]),
case("Array(Variant(UInt64, String))", &["['x'::String]", "[]"]),
];
fn rendered(columns: &Columns, name: &str) -> Result<String, String> {
let blocks: Vec<&DecodedBlock> = columns.blocks().iter().filter(|b| b.num_rows > 0).collect();
match blocks.as_slice() {
[block] => match block.column(name) {
Some(column) => Ok(format!("{column:?}")),
None => Err(format!("block declares no column '{name}'")),
},
other => Err(format!("expected one payload block, got {}", other.len())),
}
}
#[tokio::test]
async fn every_type_decodes_identically_on_both_transports() {
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let (tcp, http) = (server.tcp(), server.http());
tcp.execute("CREATE DATABASE IF NOT EXISTS wire")
.await
.unwrap();
let mut failures = Vec::new();
for (i, case) in CASES.iter().enumerate() {
let outcome = run_case(&tcp, &http, case, i).await;
match (outcome, case.known_broken) {
(Ok(()), None) | (Err(_), Some(_)) => {}
(Err(e), None) => failures.push(format!("{}: {e}", case.ch_type)),
(Ok(()), Some(reason)) => failures.push(format!(
"{} now round-trips -- drop its known_broken marker ({reason})",
case.ch_type
)),
}
}
server.stop().await;
assert!(
failures.is_empty(),
"{} cases failed:\n{}",
failures.len(),
failures.join("\n")
);
}
async fn run_case(
tcp: &UnifiedClient,
http: &UnifiedClient,
case: &Case,
index: usize,
) -> Result<(), String> {
let table = format!("wire.t{index}");
let create = format!(
"CREATE OR REPLACE TABLE {table} (c {}) ENGINE = MergeTree ORDER BY tuple()",
case.ch_type
);
tcp.execute(&create)
.await
.map_err(|e| format!("CREATE rejected: {e}"))?;
let rows: Vec<String> = case.values.iter().map(|v| format!("({v})")).collect();
let insert = format!("INSERT INTO {table} VALUES {}", rows.join(", "));
tcp.execute(&insert)
.await
.map_err(|e| format!("INSERT rejected: {e}"))?;
let sql = format!("SELECT c FROM {table}");
let over_tcp = tcp
.fetch_columns(&sql)
.await
.map_err(|e| format!("tcp SELECT failed: {e}"))?;
let over_http = http
.fetch_columns(&sql)
.await
.map_err(|e| format!("http SELECT failed: {e}"))?;
let (t, h) = (rendered(&over_tcp, "c")?, rendered(&over_http, "c")?);
if t == h {
Ok(())
} else {
Err(format!("tcp {t} != http {h}"))
}
}
const VALUE_COLUMNS: &[(&str, &str, &str, &str)] = &[
("u8", "UInt8", "0", "255"),
("u64", "UInt64", "0", "18446744073709551615"),
(
"u128",
"UInt128",
"0",
"340282366920938463463374607431768211455",
),
("i8", "Int8", "-128", "127"),
(
"i64",
"Int64",
"-9223372036854775808",
"9223372036854775807",
),
(
"i128",
"Int128",
"-170141183460469231731687303715884105728",
"0",
),
("f64", "Float64", "-1.5", "3.25"),
("b", "Bool", "true", "false"),
("s", "String", "''", "concat('a', char(0), 'b')"),
("d", "Date", "'1970-01-01'", "'2149-06-06'"),
(
"dt",
"DateTime('UTC')",
"'1970-01-01 00:00:00'",
"'2106-02-07 06:28:15'",
),
(
"dt64",
"DateTime64(3, 'UTC')",
"'1970-01-01 00:00:00.000'",
"'1970-01-01 00:00:01.500'",
),
("dec32", "Decimal(9, 2)", "'-1234567.89'", "'0.01'"),
("dec64", "Decimal(18, 4)", "'-1.0001'", "'12345678901.2345'"),
("u256", "UInt256", "0", "1"),
("ip4", "IPv4", "'0.0.0.0'", "'127.0.0.1'"),
("ip6", "IPv6", "'::'", "'::1'"),
("e8", "Enum8('a' = 1, 'b' = -2)", "'a'", "'b'"),
];
#[tokio::test]
async fn decoded_values_match_what_the_literals_asked_for() {
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let client = server.tcp();
client
.execute("CREATE DATABASE IF NOT EXISTS wire")
.await
.unwrap();
let columns: Vec<String> = VALUE_COLUMNS
.iter()
.map(|(name, ty, ..)| format!("{name} {ty}"))
.collect();
let first: Vec<&str> = VALUE_COLUMNS.iter().map(|(.., a, _)| *a).collect();
let second: Vec<&str> = VALUE_COLUMNS.iter().map(|(.., b)| *b).collect();
client
.execute(&format!(
"CREATE OR REPLACE TABLE wire.values_ (ord UInt8, {}) \
ENGINE = MergeTree ORDER BY ord",
columns.join(", ")
))
.await
.unwrap();
client
.execute(&format!(
"INSERT INTO wire.values_ VALUES (0, {}), (1, {})",
first.join(", "),
second.join(", ")
))
.await
.unwrap();
let got = client
.fetch_columns("SELECT * FROM wire.values_ ORDER BY ord")
.await;
server.stop().await;
let got = got.expect("the pinned-value row pair reads back");
assert_eq!(got.get::<u8>("u8").unwrap(), [0, 255]);
assert_eq!(got.get::<u64>("u64").unwrap(), [0, u64::MAX]);
assert_eq!(got.get::<u128>("u128").unwrap(), [0, u128::MAX]);
assert_eq!(got.get::<i8>("i8").unwrap(), [i8::MIN, i8::MAX]);
assert_eq!(got.get::<i64>("i64").unwrap(), [i64::MIN, i64::MAX]);
assert_eq!(got.get::<i128>("i128").unwrap(), [i128::MIN, 0]);
assert_eq!(got.get::<f64>("f64").unwrap(), [-1.5, 3.25]);
assert_eq!(got.get::<bool>("b").unwrap(), [true, false]);
assert_eq!(
got.get::<Vec<u8>>("s").unwrap(),
[b"".to_vec(), b"a\0b".to_vec()]
);
assert_eq!(got.get::<u16>("d").unwrap(), [0, u16::MAX]);
assert_eq!(got.get::<u32>("dt").unwrap(), [0, u32::MAX]);
assert_eq!(got.get::<i64>("dt64").unwrap(), [0, 1_500]);
assert_eq!(got.get::<i32>("dec32").unwrap(), [-123_456_789, 1]);
assert_eq!(
got.get::<i64>("dec64").unwrap(),
[-10_001, 123_456_789_012_345]
);
let u256 = got.get::<[u8; 32]>("u256").unwrap();
assert_eq!(u256[0], [0u8; 32]);
let mut one = [0u8; 32];
one[0] = 1;
assert_eq!(u256[1], one);
assert_eq!(got.get::<u32>("ip4").unwrap(), [0, 0x7F00_0001]);
let ip6 = got.get::<[u8; 16]>("ip6").unwrap();
assert_eq!(ip6[0], [0u8; 16]);
let mut loopback = [0u8; 16];
loopback[15] = 1;
assert_eq!(ip6[1], loopback);
assert_eq!(got.get::<i8>("e8").unwrap(), [1, -2]);
}
#[tokio::test]
async fn a_nested_low_cardinality_insert_matches_the_server_s_own() {
use std::sync::Arc;
use serde_json::{Map, json};
use clickhouse_dfe::dynamic::{ColumnDef, DynamicInsert, DynamicSchema};
const TYPE: &str = "Array(LowCardinality(String))";
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let client = server.tcp();
client
.execute("CREATE DATABASE IF NOT EXISTS wire")
.await
.unwrap();
for table in ["wire.lc_by_server", "wire.lc_by_encoder"] {
client
.execute(&format!(
"CREATE OR REPLACE TABLE {table} (c {TYPE}) \
ENGINE = MergeTree ORDER BY tuple()"
))
.await
.unwrap();
}
client
.execute("INSERT INTO wire.lc_by_server VALUES (['a', 'b', 'a'])")
.await
.unwrap();
let schema = Arc::new(DynamicSchema::from_columns(
"wire.lc_by_encoder",
vec![ColumnDef::new("c", TYPE)],
));
let mut insert = DynamicInsert::tcp(
client.as_tcp().expect("the tcp arm").clone(),
"wire",
"lc_by_encoder",
schema,
);
let mut row = Map::new();
row.insert("c".to_string(), json!(["a", "b", "a"]));
insert.write_map(&row).await.expect("the row encodes");
insert.end().await.expect("the insert commits");
let by_server = client
.fetch_columns("SELECT c FROM wire.lc_by_server")
.await
.expect("read back what the server wrote");
let by_encoder = client
.fetch_columns("SELECT c FROM wire.lc_by_encoder")
.await
.expect("read back what this crate wrote");
let (by_encoder, by_server) = (
rendered(&by_encoder, "c").unwrap(),
rendered(&by_server, "c").unwrap(),
);
server.stop().await;
assert_eq!(
by_encoder, by_server,
"this crate's encoder must lay out {TYPE} the way the server does"
);
}
#[tokio::test]
async fn a_root_json_array_is_rejected_raw_and_round_trips_wrapped() {
use std::sync::Arc;
use serde_json::{Map, json};
use clickhouse_dfe::dynamic::{
ColumnDef, DynamicInsert, DynamicSchema, JSON_ARRAY_WRAPPER_KEY,
};
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let client = server.tcp();
client
.execute("CREATE DATABASE IF NOT EXISTS wire")
.await
.unwrap();
client
.execute("CREATE OR REPLACE TABLE wire.tags (t JSON) ENGINE = MergeTree ORDER BY tuple()")
.await
.unwrap();
let unwrapped = client
.execute(r#"INSERT INTO wire.tags VALUES (CAST('["a","b"]', 'JSON'))"#)
.await;
let schema = Arc::new(DynamicSchema::from_columns(
"wire.tags",
vec![ColumnDef::new("t", "JSON")],
));
let mut insert = DynamicInsert::tcp(
client.as_tcp().expect("the tcp arm").clone(),
"wire",
"tags",
schema,
);
let mut row = Map::new();
row.insert("t".to_string(), json!(["a", "b"]));
insert.write_map(&row).await.expect("the row encodes");
insert.end().await.expect("the insert commits");
let read = client.fetch_columns("SELECT t FROM wire.tags").await;
server.stop().await;
let err = unwrapped.expect_err("a root array is not a JSON document the column accepts");
assert!(
format!("{err}").contains("117"),
"the server must refuse it as code 117, got: {err}"
);
assert_eq!(
read.expect("the wrapped document reads back")
.get::<String>("t")
.unwrap(),
[format!(r#"{{"{JSON_ARRAY_WRAPPER_KEY}":["a","b"]}}"#)]
);
}
const JSON_DOCUMENTS: &[(&str, &str)] = &[
("empty object", r"{}"),
(
"ecs-shaped event",
r#"{"@timestamp":"2026-09-02T08:00:00Z","host":{"name":"h1"},"tags":["beats","filebeat"]}"#,
),
("dotted key", r#"{"a.b":1}"#),
("nested object", r#"{"a":{"b":1}}"#),
("null inside", r#"{"a":null,"b":1}"#),
("empty string value", r#"{"a":""}"#),
("unicode and emoji", r#"{"ünïcode":"héllo","emoji":"🎯"}"#),
(
"escapes",
r#"{"quote":"he said \"hi\"","back\\slash":"c:\\tmp","newline":"a\nb","tab":"a\tb"}"#,
),
(
"integer bounds",
r#"{"i":-9223372036854775808,"u":18446744073709551615}"#,
),
("float forms", r#"{"e":1.5e300,"neg":-0.0,"small":1e-300}"#),
("array of objects", r#"{"items":[{"id":1},{"id":2}]}"#),
("nested arrays", r#"{"m":[[1,2],[3]]}"#),
("deep nesting", r#"{"a":{"b":{"c":{"d":{"e":{"f":1}}}}}}"#),
];
#[tokio::test]
async fn json_documents_round_trip_as_the_server_stores_them() {
use std::sync::Arc;
use serde_json::{Map, Value};
use clickhouse_dfe::dynamic::{ColumnDef, DynamicInsert, DynamicSchema};
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let client = server.tcp();
client
.execute("CREATE DATABASE IF NOT EXISTS wire")
.await
.unwrap();
let mut failures = Vec::new();
for (i, (name, document)) in JSON_DOCUMENTS.iter().enumerate() {
let table = format!("wire.doc{i}");
client
.execute(&format!(
"CREATE OR REPLACE TABLE {table} (d JSON) ENGINE = MergeTree ORDER BY tuple()"
))
.await
.unwrap();
let quoted = document.replace('\\', r"\\").replace('\'', r"\'");
if let Err(e) = client
.execute(&format!(
"INSERT INTO {table} VALUES (CAST('{quoted}', 'JSON'))"
))
.await
{
failures.push(format!("{name}: the server rejected its own literal: {e}"));
continue;
}
let schema = Arc::new(DynamicSchema::from_columns(
&table,
vec![ColumnDef::new("d", "JSON")],
));
let mut insert = DynamicInsert::tcp(
client.as_tcp().expect("the tcp arm").clone(),
"wire",
&format!("doc{i}"),
schema,
);
let parsed: Value = serde_json::from_str(document).expect("the case is valid JSON");
let mut row = Map::new();
row.insert("d".to_string(), parsed);
if let Err(e) = insert.write_map(&row).await {
failures.push(format!("{name}: encode failed: {e}"));
continue;
}
if let Err(e) = insert.end().await {
failures.push(format!("{name}: insert failed: {e}"));
continue;
}
match client
.fetch_columns(&format!("SELECT d FROM {table} ORDER BY toString(d)"))
.await
{
Ok(read) => {
let docs = read
.get::<String>("d")
.expect("a JSON column reads as text");
match docs.as_slice() {
[by_encoder, by_server] if by_encoder == by_server => {}
[a, b] => failures.push(format!("{name}: encoder {a} != server {b}")),
other => failures.push(format!("{name}: expected 2 rows, got {}", other.len())),
}
}
Err(e) => failures.push(format!("{name}: read failed: {e}")),
}
}
server.stop().await;
assert!(
failures.is_empty(),
"{} document(s) failed:\n{}",
failures.len(),
failures.join("\n")
);
}
#[tokio::test]
async fn an_oversized_json_number_is_stored_as_a_lossy_float() {
use std::sync::Arc;
use serde_json::{Map, Value};
use clickhouse_dfe::dynamic::{ColumnDef, DynamicInsert, DynamicSchema};
const HUGE: &str = r#"{"huge":123456789012345678901234567890}"#;
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let client = server.tcp();
client
.execute("CREATE DATABASE IF NOT EXISTS wire")
.await
.unwrap();
client
.execute("CREATE OR REPLACE TABLE wire.huge (d JSON) ENGINE = MergeTree ORDER BY tuple()")
.await
.unwrap();
let by_server = client
.execute(&format!(
"INSERT INTO wire.huge VALUES (CAST('{HUGE}', 'JSON'))"
))
.await;
let schema = Arc::new(DynamicSchema::from_columns(
"wire.huge",
vec![ColumnDef::new("d", "JSON")],
));
let mut insert = DynamicInsert::tcp(
client.as_tcp().expect("the tcp arm").clone(),
"wire",
"huge",
schema,
);
let parsed: Value = serde_json::from_str(HUGE).expect("the literal is valid JSON");
let mut row = Map::new();
row.insert("d".to_string(), parsed);
insert.write_map(&row).await.expect("the row encodes");
insert.end().await.expect("the insert commits");
let read = client.fetch_columns("SELECT d FROM wire.huge").await;
server.stop().await;
let server_err = by_server.expect_err("a CAST refuses the oversized literal");
assert!(
format!("{server_err}").contains("117"),
"expected code 117, got: {server_err}"
);
assert_eq!(
read.expect("the inserted document reads back")
.get::<String>("d")
.unwrap(),
[r#"{"huge":1.2345678901234568e29}"#]
);
}
#[tokio::test]
async fn a_json_path_that_changes_type_between_rows_reads_back() {
use std::sync::Arc;
use serde_json::{Map, json};
use clickhouse_dfe::dynamic::{ColumnDef, DynamicInsert, DynamicSchema};
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let client = server.tcp();
client
.execute("CREATE DATABASE IF NOT EXISTS wire")
.await
.unwrap();
client
.execute("CREATE OR REPLACE TABLE wire.mixed (d JSON) ENGINE = MergeTree ORDER BY tuple()")
.await
.unwrap();
let schema = Arc::new(DynamicSchema::from_columns(
"wire.mixed",
vec![ColumnDef::new("d", "JSON")],
));
let mut insert = DynamicInsert::tcp(
client.as_tcp().expect("the tcp arm").clone(),
"wire",
"mixed",
schema,
);
for value in [json!({"a": 1}), json!({"a": "x"}), json!({"a": [1, 2]})] {
let mut row = Map::new();
row.insert("d".to_string(), value);
insert.write_map(&row).await.expect("the row encodes");
}
insert.end().await.expect("the insert commits");
let read = client
.fetch_columns("SELECT d FROM wire.mixed ORDER BY toString(d)")
.await;
server.stop().await;
let mut docs = read
.expect("the mixed-type column reads back")
.get::<String>("d")
.unwrap();
docs.sort();
assert_eq!(docs, [r#"{"a":"x"}"#, r#"{"a":1}"#, r#"{"a":[1,2]}"#]);
}
#[tokio::test]
async fn json_reads_with_or_without_the_string_flag() {
const SQL: &str = r#"SELECT CAST('{"a":1}', 'JSON') AS doc"#;
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let with_flag = server.tcp().fetch_columns(SQL).await;
let without = TcpClient::new(format!("127.0.0.1:{}", server.native_port))
.with_json_as_string(false)
.query(SQL)
.fetch_blocks()
.await;
server.stop().await;
assert_eq!(
with_flag
.expect("the default reads JSON")
.get::<String>("doc")
.unwrap(),
[r#"{"a":1}"#]
);
let blocks = without.expect("V2 serialisation reads without the flag");
let payload: Vec<String> = blocks
.iter()
.filter(|b| b.num_rows > 0)
.flat_map(|b| b.column_as::<String>("doc").expect("doc reads as text"))
.collect();
assert_eq!(payload, [r#"{"a":1}"#]);
}
#[tokio::test]
async fn a_sparse_column_reads_back_on_both_transports() {
const TOTAL: &str = "SELECT sum(c) AS total FROM wire.sparse";
const RAW: &str = "SELECT c FROM wire.sparse ORDER BY n LIMIT 6000";
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let client = server.tcp();
client
.execute("CREATE DATABASE IF NOT EXISTS wire")
.await
.unwrap();
client
.execute(
"CREATE OR REPLACE TABLE wire.sparse (n UInt64, c UInt64) \
ENGINE = MergeTree ORDER BY n \
SETTINGS ratio_of_defaults_for_sparse_serialization = 0.9",
)
.await
.unwrap();
client
.execute(
"INSERT INTO wire.sparse \
SELECT number, if(number = 5000, 42, 0) FROM system.numbers LIMIT 10000",
)
.await
.unwrap();
client
.execute("OPTIMIZE TABLE wire.sparse FINAL")
.await
.unwrap();
let over_tcp = client.fetch_columns(TOTAL).await.expect("tcp reads it");
let over_http = server
.http()
.fetch_columns(TOTAL)
.await
.expect("http reads it");
let values = client
.fetch_columns(RAW)
.await
.expect("tcp reads the column")
.get::<u64>("c")
.unwrap();
server.stop().await;
assert_eq!(over_tcp.get::<u64>("total").unwrap(), [42]);
assert_eq!(over_http.get::<u64>("total").unwrap(), [42]);
assert_eq!(values.len(), 6000);
assert_eq!(values[5000], 42, "the one non-default row");
assert_eq!(values[0], 0);
}
#[tokio::test]
async fn a_multi_block_result_reads_back_whole_and_in_order() {
const SQL: &str = "SELECT number AS n FROM system.numbers LIMIT 200000";
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let over_tcp = server.tcp().fetch_columns(SQL).await.unwrap();
let over_http = server.http().fetch_columns(SQL).await.unwrap();
let ns = over_tcp.get::<u64>("n").unwrap();
let http_ns = over_http.get::<u64>("n").unwrap();
server.stop().await;
assert_eq!(ns.len(), 200_000, "every row must arrive");
assert_eq!(ns[0], 0);
assert_eq!(ns[199_999], 199_999);
assert_eq!(http_ns, ns, "transports disagree");
}
#[tokio::test]
async fn a_mid_stream_exception_surfaces_rather_than_truncating() {
const SQL: &str = "SELECT throwIf(number = 50000) FROM system.numbers LIMIT 100000";
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let err = server
.tcp()
.fetch_columns(SQL)
.await
.expect_err("the server throws part-way through");
server.stop().await;
assert!(
format!("{err}").contains("Value passed to 'throwIf'"),
"the server's own message must reach the caller: {err}"
);
}
#[tokio::test]
async fn a_failed_query_leaves_the_connection_usable() {
require_docker!();
let _one = ONE_SERVER.acquire().await.unwrap();
let server = server().await;
let client = server.tcp();
let _ = client.fetch_columns("SELECT * FROM no_such_table").await;
let ok = client.fetch_columns("SELECT 1 AS n").await.unwrap();
let n = ok.get::<u8>("n").unwrap();
server.stop().await;
assert_eq!(n, [1]);
}