Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cf3b531
Update from main branch
slvrtrn Jun 20, 2025
197ce8b
Merge remote-tracking branch 'origin' into rbwnat-insert
slvrtrn Jun 26, 2025
1b24497
Initial impl of serializer validation
slvrtrn Jun 27, 2025
dcca54a
Add insert tests, reorganize existing RBWNAT tests
slvrtrn Jun 27, 2025
e8b9783
Merge remote-tracking branch 'origin' into rbwnat-insert
slvrtrn Jul 3, 2025
ab6047e
Fix docs issues
slvrtrn Jul 3, 2025
901659e
Fix cargo fmt
slvrtrn Jul 3, 2025
8d76d18
Fix `Insert::end` ownership
slvrtrn Jul 3, 2025
8ecfbe7
Move row_metadata around, don't use header for raw RowBinary
slvrtrn Jul 3, 2025
ae9a4b1
Support wrong struct field order with inserts
slvrtrn Jul 3, 2025
a0dbeb9
Add tests for various third-party `*Map` types
slvrtrn Jul 3, 2025
2284cce
Merge remote-tracking branch 'origin' into rbwnat-insert
slvrtrn Jul 29, 2025
d9c3125
cargo fmt
slvrtrn Jul 29, 2025
36350ac
Merge remote-tracking branch 'origin' into rbwnat-insert
slvrtrn Jul 30, 2025
59be19f
Add more tests
slvrtrn Jul 30, 2025
ac2ba48
Fix tuple test
slvrtrn Jul 30, 2025
3be06b0
Update README.md
slvrtrn Jul 30, 2025
b383bee
Adjust mocked_insert benchmark, add more tests
slvrtrn Jul 30, 2025
c5699bd
Fix clippy
slvrtrn Jul 30, 2025
170a545
Allow single element loop with disabled features
slvrtrn Jul 30, 2025
300de26
Fix from_utf8_lossy import in tests
slvrtrn Sep 22, 2025
fc97158
Merge remote-tracking branch 'origin' into rbwnat-insert
slvrtrn Sep 22, 2025
664b440
Fix futures imports
slvrtrn Sep 22, 2025
08de9a4
Fix rustfmt
slvrtrn Sep 22, 2025
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
6 changes: 3 additions & 3 deletions benches/mocked_insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async fn run_insert(client: Client, addr: SocketAddr, iters: u64) -> Result<Dura
let _server = common::start_server(addr, serve).await;

let start = Instant::now();
let mut insert = client.insert("table")?;
let mut insert = client.insert("table").await?;

for _ in 0..iters {
insert.write(&SomeRow::sample()).await?;
Expand All @@ -70,15 +70,15 @@ async fn run_inserter<const WITH_PERIOD: bool>(
let _server = common::start_server(addr, serve).await;

let start = Instant::now();
let mut inserter = client.inserter("table")?.with_max_rows(iters);
let mut inserter = client.inserter("table").with_max_rows(iters);

if WITH_PERIOD {
// Just to measure overhead, not to actually use it.
inserter = inserter.with_period(Some(Duration::from_secs(1000)));
}

for _ in 0..iters {
inserter.write(&SomeRow::sample())?;
inserter.write(&SomeRow::sample()).await?;
inserter.commit().await?;
}

Expand Down
32 changes: 16 additions & 16 deletions benches/select_market_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,21 @@ async fn prepare_data() {
client
.query(
r#"
CREATE TABLE IF NOT EXISTS l2_book_log
(
`instrument_id` UInt32 CODEC(T64, Default),
`received_time` DateTime64(9) CODEC(DoubleDelta, Default),
`exchange_time` Nullable(DateTime64(9)) CODEC(DoubleDelta, Default),
`sequence_no` Nullable(UInt64) CODEC(DoubleDelta, Default),
`trace_id` UInt64 CODEC(DoubleDelta, Default),
`side` Enum8('Bid' = 0, 'Ask' = 1),
`price` Float64,
`amount` Float64,
`is_eot` Bool
)
ENGINE = MergeTree
PRIMARY KEY (instrument_id, received_time)
"#,
CREATE TABLE IF NOT EXISTS l2_book_log
(
`instrument_id` UInt32 CODEC(T64, Default),
`received_time` DateTime64(9) CODEC(DoubleDelta, Default),
`exchange_time` Nullable(DateTime64(9)) CODEC(DoubleDelta, Default),
`sequence_no` Nullable(UInt64) CODEC(DoubleDelta, Default),
`trace_id` UInt64 CODEC(DoubleDelta, Default),
`side` Enum8('Bid' = 0, 'Ask' = 1),
`price` Float64,
`amount` Float64,
`is_eot` Bool
)
ENGINE = MergeTree
PRIMARY KEY (instrument_id, received_time)
"#,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using raw string literals is also something idiomatic to SQLx, though strictly speaking this is only necessary for when you need to quote identifiers as it avoids the need for escapes: https://docs.rs/sqlx/latest/sqlx/macro.query.html#type-overrides-output-columns

Since it appears ClickHouse uses backticks instead of double quotes for quoted identifiers, there's not much benefit to using raw string literals here.

)
.execute()
.await
Expand All @@ -71,7 +71,7 @@ async fn prepare_data() {
return;
}

let mut insert = client.insert("l2_book_log").unwrap();
let mut insert = client.insert("l2_book_log").await.unwrap();

for i in 0..10_000_000 {
insert
Expand Down
2 changes: 1 addition & 1 deletion examples/async_insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async fn main() -> Result<()> {
.execute()
.await?;

let mut insert = client.insert(table_name)?;
let mut insert = client.insert(table_name).await?;
insert
.write(&Event {
timestamp: now(),
Expand Down
2 changes: 1 addition & 1 deletion examples/clickhouse_cloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async fn main() -> clickhouse::error::Result<()> {
.execute()
.await?;

let mut insert = client.insert(table_name)?;
let mut insert = client.insert(table_name).await?;
insert
.write(&Data {
id: 42,
Expand Down
2 changes: 1 addition & 1 deletion examples/data_types_derive_containers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ async fn main() -> Result<()> {
.execute()
.await?;

let mut insert = client.insert(table_name)?;
let mut insert = client.insert(table_name).await?;
insert.write(&Row::new()).await?;
insert.end().await?;

Expand Down
2 changes: 1 addition & 1 deletion examples/data_types_derive_simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ async fn main() -> Result<()> {
.execute()
.await?;

let mut insert = client.insert(table_name)?;
let mut insert = client.insert(table_name).await?;
insert.write(&Row::new()).await?;
insert.end().await?;

Expand Down
2 changes: 1 addition & 1 deletion examples/data_types_new_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async fn main() -> Result<()> {
.to_string(),
};

let mut insert = client.insert(table_name)?;
let mut insert = client.insert(table_name).await?;
insert.write(&row).await?;
insert.end().await?;

Expand Down
2 changes: 1 addition & 1 deletion examples/data_types_variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async fn main() -> Result<()> {
.execute()
.await?;

let mut insert = client.insert(table_name)?;
let mut insert = client.insert(table_name).await?;
let rows_to_insert = get_rows();
for row in rows_to_insert {
insert.write(&row).await?;
Expand Down
2 changes: 1 addition & 1 deletion examples/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async fn main() -> Result<()> {
Error = 4,
}

let mut insert = client.insert("event_log")?;
let mut insert = client.insert("event_log").await?;
insert
.write(&Event {
timestamp: now(),
Expand Down
8 changes: 4 additions & 4 deletions examples/inserter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct MyRow {
// In other words, this pattern is applicable for ETL-like tasks.
async fn dense(client: &Client, mut rx: Receiver<u32>) -> Result<()> {
let mut inserter = client
.inserter(TABLE_NAME)?
.inserter(TABLE_NAME)
// We limit the number of rows to be inserted in a single `INSERT` statement.
// We use small value (100) for the example only.
// See documentation of `with_max_rows` for details.
Expand All @@ -32,7 +32,7 @@ async fn dense(client: &Client, mut rx: Receiver<u32>) -> Result<()> {
.with_max_bytes(1_048_576);

while let Some(no) = rx.recv().await {
inserter.write(&MyRow { no })?;
inserter.write(&MyRow { no }).await?;
inserter.commit().await?;
}

Expand All @@ -47,7 +47,7 @@ async fn dense(client: &Client, mut rx: Receiver<u32>) -> Result<()> {
// Some rows are arriving one by one with delay, some batched.
async fn sparse(client: &Client, mut rx: Receiver<u32>) -> Result<()> {
let mut inserter = client
.inserter(TABLE_NAME)?
.inserter(TABLE_NAME)
// Slice the stream into chunks (one `INSERT` per chunk) by time.
// See documentation of `with_period` for details.
.with_period(Some(Duration::from_millis(100)))
Expand Down Expand Up @@ -85,7 +85,7 @@ async fn sparse(client: &Client, mut rx: Receiver<u32>) -> Result<()> {
Err(TryRecvError::Disconnected) => break,
};

inserter.write(&MyRow { no })?;
inserter.write(&MyRow { no }).await?;
inserter.commit().await?;

// You can use result of `commit()` to get the number of rows inserted.
Expand Down
2 changes: 1 addition & 1 deletion examples/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async fn make_select(client: &Client) -> Result<Vec<SomeRow>> {
}

async fn make_insert(client: &Client, data: &[SomeRow]) -> Result<()> {
let mut insert = client.insert("who cares")?;
let mut insert = client.insert("who cares").await?;
for row in data {
insert.write(row).await?;
}
Expand Down
2 changes: 1 addition & 1 deletion examples/session_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async fn main() -> Result<()> {
i: i32,
}

let mut insert = client.insert(table_name)?;
let mut insert = client.insert(table_name).await?;
insert.write(&MyRow { i: 42 }).await?;
insert.end().await?;

Expand Down
6 changes: 3 additions & 3 deletions examples/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async fn ddl(client: &Client) -> Result<()> {
}

async fn insert(client: &Client) -> Result<()> {
let mut insert = client.insert("some")?;
let mut insert = client.insert("some").await?;
for i in 0..1000 {
insert.write(&MyRow { no: i, name: "foo" }).await?;
}
Expand All @@ -42,12 +42,12 @@ async fn insert(client: &Client) -> Result<()> {
#[cfg(feature = "inserter")]
async fn inserter(client: &Client) -> Result<()> {
let mut inserter = client
.inserter("some")?
.inserter("some")
.with_max_rows(100_000)
.with_period(Some(std::time::Duration::from_secs(15)));

for i in 0..1000 {
inserter.write(&MyRow { no: i, name: "foo" })?;
inserter.write(&MyRow { no: i, name: "foo" }).await?;
inserter.commit().await?;
}

Expand Down
59 changes: 38 additions & 21 deletions src/insert.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
use std::{future::Future, marker::PhantomData, mem, panic, pin::Pin, time::Duration};

use crate::headers::{with_authentication, with_request_headers};
use crate::row_metadata::RowMetadata;
use crate::rowbinary::{serialize_row_binary, serialize_with_validation};
use crate::{
error::{Error, Result},
request_body::{ChunkSender, RequestBody},
response::Response,
row::{self, Row},
Client, Compression,
};
use bytes::{Bytes, BytesMut};
use clickhouse_types::put_rbwnat_columns_header;
use hyper::{self, Request};
use replace_with::replace_with_or_abort;
use serde::Serialize;
use std::sync::Arc;
use std::{future::Future, marker::PhantomData, mem, panic, pin::Pin, time::Duration};
use tokio::{
task::JoinHandle,
time::{Instant, Sleep},
};
use url::Url;

use crate::headers::{with_authentication, with_request_headers};
use crate::{
error::{Error, Result},
request_body::{ChunkSender, RequestBody},
response::Response,
row::{self, Row},
rowbinary, Client, Compression,
};

// The desired max frame size.
const BUFFER_SIZE: usize = 256 * 1024;
// Threshold to send a chunk. Should be slightly less than `BUFFER_SIZE`
Expand All @@ -37,6 +39,7 @@ const_assert!(BUFFER_SIZE.is_power_of_two()); // to use the whole buffer's capac
pub struct Insert<T> {
state: InsertState,
buffer: BytesMut,
row_metadata: Option<Arc<RowMetadata>>,
#[cfg(feature = "lz4")]
compression: Compression,
send_timeout: Option<Duration>,
Expand Down Expand Up @@ -119,8 +122,7 @@ macro_rules! timeout {
}

impl<T> Insert<T> {
// TODO: remove Result
pub(crate) fn new(client: &Client, table: &str) -> Result<Self>
pub(crate) fn new(client: &Client, table: &str, row_metadata: Option<Arc<RowMetadata>>) -> Self
where
T: Row,
{
Expand All @@ -129,9 +131,14 @@ impl<T> Insert<T> {

// TODO: what about escaping a table name?
// https://clickhouse.com/docs/en/sql-reference/syntax#identifiers
let sql = format!("INSERT INTO {}({}) FORMAT RowBinary", table, fields);
let format = if row_metadata.is_some() {
"RowBinaryWithNamesAndTypes"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see the format types defined as an enum, or at least constants somewhere. That's something I could do. If we want the user to be able to specify formats that we don't know about (e.g if new ones get added), there's options for that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, this is internal only; we can add some popular formats as an enum provided by the crate. This might be useful with fetch_bytes: https://github.com/ClickHouse/clickhouse-rs/blob/main/examples/stream_arbitrary_format_rows.rs
and its INSERT counterpart that is to be implemented: #174

So perhaps it can be introduced in the scope of #174 indeed. However: it should implement Into<String> and not break the existing API expecting an impl Into<String>, cause we want to export these enums as just a hint on what is available, and this library might not be in sync with the latest CH release that adds a new format, which the end user might want to use ASAP.

} else {
"RowBinary"
};
let sql = format!("INSERT INTO {table}({fields}) FORMAT {format}");

Ok(Self {
Self {
state: InsertState::NotStarted {
client: Box::new(client.clone()),
sql,
Expand All @@ -143,7 +150,8 @@ impl<T> Insert<T> {
end_timeout: None,
sleep: Box::pin(tokio::time::sleep(Duration::new(0, 0))),
_marker: PhantomData,
})
row_metadata,
}
}

/// Sets timeouts for different operations.
Expand Down Expand Up @@ -194,6 +202,7 @@ impl<T> Insert<T> {
/// socket.
///
/// Close to:
///
/// ```ignore
/// async fn write<T>(&self, row: &T) -> Result<usize>;
/// ```
Expand All @@ -205,10 +214,11 @@ impl<T> Insert<T> {
/// used anymore.
///
/// # Panics
///
/// If called after the previous call that returned an error.
pub fn write<'a>(&'a mut self, row: &T) -> impl Future<Output = Result<()>> + 'a + Send
where
T: Serialize,
T: Serialize + Row,
{
let result = self.do_write(row);

Expand All @@ -224,7 +234,7 @@ impl<T> Insert<T> {
#[inline(always)]
pub(crate) fn do_write(&mut self, row: &T) -> Result<usize>
where
T: Serialize,
T: Serialize + Row,
{
match self.state {
InsertState::NotStarted { .. } => self.init_request(),
Expand All @@ -233,7 +243,10 @@ impl<T> Insert<T> {
}?;

let old_buf_size = self.buffer.len();
let result = rowbinary::serialize_into(&mut self.buffer, row);
let result = match &self.row_metadata {
Some(metadata) => serialize_with_validation(&mut self.buffer, row, metadata),
None => serialize_row_binary(&mut self.buffer, row),
};
let written = self.buffer.len() - old_buf_size;

if result.is_err() {
Expand All @@ -249,7 +262,7 @@ impl<T> Insert<T> {
/// successfully, including all materialized views and quorum writes.
///
/// NOTE: If it isn't called, the whole `INSERT` is aborted.
pub async fn end(mut self) -> Result<()> {
pub async fn end(&mut self) -> Result<()> {
Comment thread
slvrtrn marked this conversation as resolved.
Outdated
if !self.buffer.is_empty() {
self.send_chunk().await?;
}
Expand All @@ -264,7 +277,6 @@ impl<T> Insert<T> {
// It's difficult to determine when allocations occur.
// So, instead we control it manually here and rely on the system allocator.
let chunk = self.take_and_prepare_chunk()?;

let sender = self.state.sender().unwrap(); // checked above

let is_timed_out = match timeout!(self, send_timeout, sender.send(chunk)) {
Expand Down Expand Up @@ -366,6 +378,11 @@ impl<T> Insert<T> {
let handle =
tokio::spawn(async move { Response::new(future, Compression::None).finish().await });

put_rbwnat_columns_header(
&self.row_metadata.as_ref().unwrap().columns,
&mut self.buffer,
)?;

self.state = InsertState::Active { handle, sender };
Ok(())
}
Expand Down
Loading