Files
zeekay 86788566d1 datastore: rebrand the Rust client and speak the Datastore wire protocol
Cut from the upstream v0.13.3 release tag (not HEAD) so this is a pure
rename with no behaviour change. v0.13.3 is what our consumers already
resolve to: insights/rust's Cargo.lock pins clickhouse 0.13.3 and its
manifest already declares `datastore` at 0.13.x.

The load-bearing change is the HTTP auth header rename. The Datastore
server reads only X-Datastore-User / X-Datastore-Key; the upstream names
return `401 Code: 194 Authentication failed`. Every un-renamed HTTP
client is therefore unable to authenticate at all — this is a
correctness fix, not branding.

- crate `clickhouse` -> `datastore`, `clickhouse-derive` -> `datastore-derive`;
  versions preserved exactly (0.13.3 / 0.2.0).
- derive macro emits `datastore::Row` in lockstep, so downstream
  `#[derive(Row)]` keeps resolving.
- User-Agent reports `datastore-rs/{version}`.
- test-harness env vars CLICKHOUSE_* -> DATASTORE_*; compose uses
  ghcr.io/hanzoai/datastore with /etc/datastore-server config paths.
- doc links point at docs.hanzo.ai/datastore.
- `publish = false`: the `datastore` name on crates.io belongs to an
  unrelated crate, so consume this by git tag, never by version range.

CHANGELOG is truncated to a fork notice rather than find-replaced —
rewriting upstream release history would manufacture changes Datastore
never shipped. References to upstream issues keep their numbers.
2026-07-26 11:30:32 -07:00

174 lines
5.8 KiB
Rust

use std::time::Duration;
use serde::{Deserialize, Serialize};
use tokio::{
sync::mpsc::{self, error::TryRecvError, Receiver},
time::timeout,
};
use datastore::{error::Result, sql::Identifier, Client, Row};
const TABLE_NAME: &str = "chrs_inserter";
#[derive(Debug, Row, Serialize, Deserialize)]
struct MyRow {
no: u32,
}
// Pattern 1: dense streams
// ------------------------
// This pattern is useful when the stream is dense, i.e. with no/small pauses
// between rows. For instance, when reading from a file or another database.
// 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)?
// 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.
.with_max_rows(100)
// You can also use other limits. For instance, limit by the size.
// First reached condition will end the current `INSERT`.
.with_max_bytes(1_048_576);
while let Some(no) = rx.recv().await {
inserter.write(&MyRow { no })?;
inserter.commit().await?;
}
inserter.end().await?;
Ok(())
}
// Pattern 2: sparse streams
// -------------------------
// This pattern is useful when the stream is sparse, i.e. with pauses between
// rows. For instance, when streaming a real-time stream of events into CH.
// 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)?
// 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)))
// If you have a lot of parallel inserters (e.g. on multiple nodes),
// it's reasonable to add some bias to the period to spread the load.
.with_period_bias(0.1)
// We also can use other limits. This is useful when the stream is
// recovered after a long time of inactivity (e.g. restart of service or CH).
.with_max_rows(500_000);
loop {
let no = match rx.try_recv() {
Ok(event) => event,
Err(TryRecvError::Empty) => {
// If there is no available events, we should wait for the next one.
// However, we don't know when the next event will arrive.
// So, we should wait no longer than the left time of the current period.
let time_left = inserter.time_left().expect("with_period is set");
// Note: `rx.recv()` must be cancel safe for your channel.
// This is true for popular `tokio`, `futures-channel`, `flume` channels.
match timeout(time_left, rx.recv()).await {
Ok(Some(event)) => event,
// The stream is closed.
Ok(None) => break,
// Timeout
Err(_) => {
// If the period is over, we allow the inserter to end the current `INSERT`
// statement. If no `INSERT` is in progress, this call is no-op.
inserter.commit().await?;
continue;
}
}
}
Err(TryRecvError::Disconnected) => break,
};
inserter.write(&MyRow { no })?;
inserter.commit().await?;
// You can use result of `commit()` to get the number of rows inserted.
// It's useful not only for statistics but also to implement
// at-least-once delivery by sending this info back to the sender,
// where all unacknowledged events should be stored in this case.
}
inserter.end().await?;
Ok(())
}
fn spawn_data_generator(n: u32, sparse: bool) -> Receiver<u32> {
let (tx, rx) = mpsc::channel(1000);
tokio::spawn(async move {
for no in 0..n {
if sparse {
let delay_ms = if no % 100 == 0 { 20 } else { 2 };
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
tx.send(no).await.unwrap();
}
});
rx
}
async fn fetch_batches(client: &Client) -> Result<Vec<(String, u64)>> {
client
.query(
"SELECT toString(insertion_time), count()
FROM ?
GROUP BY insertion_time
ORDER BY insertion_time",
)
.bind(Identifier(TABLE_NAME))
.fetch_all::<(String, u64)>()
.await
}
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::default().with_url("http://localhost:8123");
client
.query(
"CREATE OR REPLACE TABLE ? (
no UInt32,
insertion_time DateTime64(6) DEFAULT now64(6)
)
ENGINE = MergeTree
ORDER BY no",
)
.bind(Identifier(TABLE_NAME))
.execute()
.await?;
println!("Pattern 1: dense streams");
let rx = spawn_data_generator(1000, false);
dense(&client, rx).await?;
// Prints 10 batches with 100 rows in each.
for (insertion_time, count) in fetch_batches(&client).await? {
println!("{}: {} rows", insertion_time, count);
}
client
.query("TRUNCATE TABLE ?")
.bind(Identifier(TABLE_NAME))
.execute()
.await?;
println!("\nPattern 2: sparse streams");
let rx = spawn_data_generator(1000, true);
sparse(&client, rx).await?;
// Prints batches every 100±10ms.
for (insertion_time, count) in fetch_batches(&client).await? {
println!("{}: {} rows", insertion_time, count);
}
Ok(())
}