Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "clickhouse"
version = "0.15.2"
version = "0.15.3"
description = "Official Rust client for ClickHouse DB"
keywords = ["clickhouse", "database", "driver", "tokio", "hyper"]
readme = "README.md"
Expand Down
7 changes: 7 additions & 0 deletions ext-arrow/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased] - ReleaseDate

### Fixed

* Changed how product info is added internally so it doesn't end up in the user agent string ahead of any user-added
product info. ([#484])

[#484]: https://github.com/ClickHouse/clickhouse-rs/pull/484

## [0.1.0] - 2026-06-01

Initial release.
Expand Down
2 changes: 1 addition & 1 deletion ext-arrow/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ tokio = "1"
tracing = "0.1"

[dependencies.clickhouse]
version = "0.15.1"
version = "0.15.3"
path = ".."
20 changes: 20 additions & 0 deletions ext-arrow/clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
disallowed-macros = [
# https://github.com/tokio-rs/tracing/issues/2448
"tracing::enabled"
]

# https://github.com/ClickHouse/clickhouse-rs/issues/469
[[disallowed-methods]]
Comment thread
abonander marked this conversation as resolved.
Resolved
path = "clickhouse::Client::with_product_info"
reason = "will place product info at front of user agent string, ahead of the app's product info"
replacement = "with_stack_product_info"

[[disallowed-methods]]
path = "clickhouse::query::Query::with_product_info"
reason = "will place product info at front of user agent string, ahead of the app's product info"
replacement = "with_stack_product_info"

[[disallowed-methods]]
path = "clickhouse::insert_formatted::InsertFormatted::with_product_info"
reason = "will place product info at front of user agent string, ahead of the app's product info"
replacement = "with_stack_product_info"
6 changes: 3 additions & 3 deletions ext-arrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ impl ArrowQueryExt for Query {
// Prevent ClickHouse from double-compressing
.with_setting("output_format_arrow_compression_method", "none")
// Add specific product info to let us track Arrow adoption
.with_product_info("clickhouse-ext-arrow", _priv::CARGO_PKG_VERSION)
.with_stack_product_info("clickhouse-ext-arrow", _priv::CARGO_PKG_VERSION)
.fetch_bytes("ArrowStream")?,
buffer: Buffer::default(),
decoder: StreamDecoder::new(),
Expand Down Expand Up @@ -287,7 +287,7 @@ impl InsertState {
let insert = client
.insert_formatted_with(query_string)
// Add specific product info to let us track Arrow adoption
.with_product_info("clickhouse-ext-arrow", _priv::CARGO_PKG_VERSION)
.with_stack_product_info("clickhouse-ext-arrow", _priv::CARGO_PKG_VERSION)
.buffered();

tracing::record_all!(insert._priv_span(), db.collection.name = table);
Expand All @@ -302,7 +302,7 @@ impl InsertState {
let insert = client
.insert_formatted_with(sql)
// Add specific product info to let us track Arrow adoption
.with_product_info("clickhouse-ext-arrow", _priv::CARGO_PKG_VERSION)
.with_stack_product_info("clickhouse-ext-arrow", _priv::CARGO_PKG_VERSION)
.buffered();

*self = Self::Started(
Expand Down
41 changes: 27 additions & 14 deletions src/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,39 @@ use hyper::http::request::Builder;
use std::collections::HashMap;
use std::env::consts::OS;

fn get_user_agent(products_info: &[ProductInfo]) -> String {
fn get_user_agent(app_product_info: &[ProductInfo], stack_product_info: &[ProductInfo]) -> String {
use std::fmt::Write;

// See https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
let pkg_ver = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown");
let rust_ver = option_env!("CARGO_PKG_RUST_VERSION").unwrap_or("unknown");
let default_agent = format!("clickhouse-rs/{pkg_ver} (lv:rust/{rust_ver}; os:{OS})");
if products_info.is_empty() {
default_agent
} else {
let products = products_info
.iter()
.rev()
.map(|product_info| product_info.to_string())
.collect::<Vec<String>>()
.join(" ");
format!("{products} {default_agent}")

let mut infix = "";

let mut user_agent = String::new();

for product_info in stack_product_info.iter().chain(app_product_info).rev() {
write!(user_agent, "{infix}{product_info}")
.expect("BUG: formatting to a string should be infallible");

infix = " ";
}

write!(
user_agent,
"{infix}clickhouse-rs/{pkg_ver} (lv:rust/{rust_ver}; os:{OS})"
)
.expect("BUG: formatting to a string should be infallible");

user_agent
}

#[inline]
pub(crate) fn with_request_headers(
mut builder: Builder,
headers: &HashMap<String, String>,
products_info: &[ProductInfo],
app_product_info: &[ProductInfo],
stack_product_info: &[ProductInfo],
) -> Builder {
// Inject the OpenTelemetry trace context if the feature is enabled
#[cfg(feature = "opentelemetry")]
Expand All @@ -46,7 +56,10 @@ pub(crate) fn with_request_headers(
for (name, value) in headers {
builder = builder.header(name, value);
}
builder = builder.header(USER_AGENT.to_string(), get_user_agent(products_info));
builder = builder.header(
USER_AGENT.to_string(),
get_user_agent(app_product_info, stack_product_info),
);
builder
}

Expand Down
23 changes: 22 additions & 1 deletion src/insert_formatted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,22 @@ impl InsertFormatted {
self
}

/// NOTE: not meant for general use; prefer `.with_product_info()`.
/// Used in `clickhouse-ext-arrow` to track Arrow adoption.
/// A separate method is necessary to ensure this doesn't end up ahead of the end-user's
/// product info in the user agent string.
#[doc(hidden)]
pub fn with_stack_product_info(
Comment thread
abonander marked this conversation as resolved.
Resolved
mut self,
product_name: impl Into<String>,
product_version: impl Into<String>,
) -> Self {
self.state
.expect_client_mut()
.add_stack_product_info(product_name.into(), product_version.into());
self
}

pub(crate) fn expect_client(&self) -> &Client {
self.state.expect_client()
}
Expand Down Expand Up @@ -497,7 +513,12 @@ impl InsertFormatted {
drop(pairs);

let mut builder = Request::post(url.as_str());
builder = with_request_headers(builder, &client.headers, &client.products_info);
builder = with_request_headers(
builder,
&client.headers,
&client.app_product_info,
&client.stack_product_info,
);
builder = with_authentication(builder, &client.authentication);

let (sender, body) = RequestBody::chunked();
Expand Down
4 changes: 2 additions & 2 deletions src/insert_native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ mod tests {

let insert = client.insert_native("foo");

let product_info = &insert.writer.expect_client().products_info;
let product_info = &insert.writer.expect_client().app_product_info;
assert!(
product_info.is_empty(),
"unexpected product_info: {product_info:?}"
Expand All @@ -199,7 +199,7 @@ mod tests {
.with_product_info("foo", "1.0.0")
.with_product_info("bar", "0.1.0-alpha.1");

let product_info = &insert.writer.expect_client().products_info;
let product_info = &insert.writer.expect_client().app_product_info;

assert_eq!(
*product_info,
Expand Down
37 changes: 32 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ pub struct Client {
roles: HashSet<String>,
settings: HashMap<String, String>,
headers: HashMap<String, String>,
products_info: Vec<ProductInfo>,
// Separate product info added by the user from product info added by the stack
// (e.g. `clickhouse-ext-arrow`) so it appears in the right order in logs.
app_product_info: Vec<ProductInfo>,
stack_product_info: Vec<ProductInfo>,
validation: bool,
insert_metadata_cache: Arc<InsertMetadataCache>,

Expand Down Expand Up @@ -134,7 +137,8 @@ impl std::fmt::Debug for Client {
.field("roles", &self.roles)
.field("settings", &self.settings)
.field("headers", &self.headers.keys()) // redact values
.field("products_info", &self.products_info)
.field("app_product_info", &self.app_product_info)
.field("stack_product_info", &self.stack_product_info)
.field("validation", &self.validation)
.finish_non_exhaustive()
}
Expand All @@ -159,7 +163,8 @@ impl Client {
roles: HashSet::new(),
settings: HashMap::new(),
headers: HashMap::new(),
products_info: Vec::default(),
app_product_info: Vec::default(),
stack_product_info: Vec::default(),
validation: true,
insert_metadata_cache: Arc::new(InsertMetadataCache::default()),
#[cfg(feature = "test-util")]
Expand Down Expand Up @@ -443,8 +448,29 @@ impl Client {
self
}

/// NOTE: not meant for general use; prefer `.with_product_info()`.
/// Used in `clickhouse-ext-arrow` to track Arrow adoption.
/// A separate method is necessary to ensure this doesn't end up ahead of the end-user's
/// product info in the user agent string.
#[doc(hidden)]
pub fn with_stack_product_info(
mut self,
product_name: impl Into<String>,
product_version: impl Into<String>,
) -> Self {
self.add_stack_product_info(product_name.into(), product_version.into());
self
}

pub(crate) fn add_product_info(&mut self, product_name: String, product_version: String) {
self.products_info.push(ProductInfo {
self.app_product_info.push(ProductInfo {
name: product_name,
version: product_version,
});
}

pub(crate) fn add_stack_product_info(&mut self, product_name: String, product_version: String) {
self.stack_product_info.push(ProductInfo {
name: product_name,
version: product_version,
});
Expand Down Expand Up @@ -1079,12 +1105,13 @@ Client {
headers: [
\"X-Trace-Id\",
],
products_info: [
app_product_info: [
ProductInfo {
name: \"MyApp\",
version: \"0.0.1\",
},
],
stack_product_info: [],
validation: false,
..
}";
Expand Down
26 changes: 24 additions & 2 deletions src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,12 @@ impl Query {
*headers_mut = headers;
}

builder = with_request_headers(builder, &self.client.headers, &self.client.products_info);
builder = with_request_headers(
builder,
&self.client.headers,
&self.client.app_product_info,
&self.client.stack_product_info,
);
builder = with_authentication(builder, &self.client.authentication);

let content_length = query.len();
Expand Down Expand Up @@ -354,7 +359,6 @@ impl Query {
self
}

// Used in `clickhouse-ext-arrow` to track Arrow adoption.
/// Similar to [`Client::with_product_info()`], but for this query only.
pub fn with_product_info(
self,
Expand All @@ -367,6 +371,24 @@ impl Query {
}
}

/// NOTE: not meant for general use; prefer `.with_product_info()`.
/// Used in `clickhouse-ext-arrow` to track Arrow adoption.
/// A separate method is necessary to ensure this doesn't end up ahead of the end-user's
/// product info in the user agent string.
#[doc(hidden)]
pub fn with_stack_product_info(
self,
product_name: impl Into<String>,
product_version: impl Into<String>,
) -> Self {
Self {
client: self
.client
.with_stack_product_info(product_name, product_version),
..self
}
}

/// Specify server side parameter for query.
///
/// In queries, you can reference params as {name: type} e.g. {val: Int32}.
Expand Down
7 changes: 5 additions & 2 deletions tests/it/arrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,10 @@ async fn query_empty_response() {

#[tokio::test]
async fn ext_arrow_adds_user_agent() {
let client = prepare_database!();
let client = prepare_database!()
// Ensure the product info added by `clickhouse-ext-arrow` is at the right place;
// it shouldn't end up ahead of our end-user's product info here.
.with_product_info("clickhouse-ext-arrow-test", "0.0.0");

client.query("CREATE TABLE arrow_product_info_test(foo Int32, bar String) ENGINE = MergeTree ORDER BY foo")
.execute()
Expand Down Expand Up @@ -295,7 +298,7 @@ async fn ext_arrow_adds_user_agent() {
.unwrap();

let expected_user_agent = format!(
"clickhouse-ext-arrow/{ARROW_EXT_VER} clickhouse-rs/{PKG_VER} (lv:rust/{RUST_VER}; os:{OS})"
"clickhouse-ext-arrow-test/0.0.0 clickhouse-ext-arrow/{ARROW_EXT_VER} clickhouse-rs/{PKG_VER} (lv:rust/{RUST_VER}; os:{OS})"
);

assert_eq!(recorded_user_agents.len(), 1);
Expand Down
Loading