Files
rs/examples/stream_into_file.rs
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

79 lines
2.5 KiB
Rust

use datastore::{query::BytesCursor, Client};
use std::time::Instant;
use tokio::{fs::File, io::AsyncWriteExt};
// Examples of streaming the result of a query in an arbitrary format into a
// file. In this case, `CSVWithNamesAndTypes` format is used.
// Check also other formats in https://docs.hanzo.ai/datastore/interfaces/formats.
//
// Note: there is no need to wrap `File` into `BufWriter` because `BytesCursor`
// is buffered internally already and produces chunks of data.
const NUMBERS: u32 = 100_000;
fn query(numbers: u32) -> BytesCursor {
let client = Client::default().with_url("http://localhost:8123");
client
.query(
"SELECT number, hex(randomPrintableASCII(20)) AS hex_str
FROM system.numbers
LIMIT {limit: Int32}",
)
.param("limit", numbers)
.fetch_bytes("CSVWithNamesAndTypes")
.unwrap()
}
// Pattern 1: use the `tokio::io::copy_buf` helper.
//
// It shows integration with `tokio::io::AsyncBufWriteExt` trait.
async fn tokio_copy_buf(filename: &str) {
let mut cursor = query(NUMBERS);
let mut file = File::create(filename).await.unwrap();
tokio::io::copy_buf(&mut cursor, &mut file).await.unwrap();
}
// Pattern 2: use `BytesCursor::next()`.
async fn cursor_next(filename: &str) {
let mut cursor = query(NUMBERS);
let mut file = File::create(filename).await.unwrap();
while let Some(bytes) = cursor.next().await.unwrap() {
file.write_all(&bytes).await.unwrap();
println!("chunk of {}B written to {filename}", bytes.len());
}
}
// Pattern 3: use the `futures::(Try)StreamExt` traits.
#[cfg(feature = "futures03")]
async fn futures03_stream(filename: &str) {
use futures::TryStreamExt;
let mut cursor = query(NUMBERS);
let mut file = File::create(filename).await.unwrap();
while let Some(bytes) = cursor.try_next().await.unwrap() {
file.write_all(&bytes).await.unwrap();
println!("chunk of {}B written to {filename}", bytes.len());
}
}
#[tokio::main]
async fn main() {
let start = Instant::now();
tokio_copy_buf("output-1.csv").await;
println!("written to output-1.csv in {:?}", start.elapsed());
let start = Instant::now();
cursor_next("output-2.csv").await;
println!("written to output-2.csv in {:?}", start.elapsed());
#[cfg(feature = "futures03")]
{
let start = Instant::now();
futures03_stream("output-3.csv").await;
println!("written to output-3.csv in {:?}", start.elapsed());
}
}