use std::process::ExitCode;
use serde::{Deserialize, Serialize};
use ytsaurus_client::{Client, ClientError, TableRow};
use ytsaurus_yson::{YsonFormat, YsonNode, YsonValue, to_string};
const BASE: &str = "//tmp/ytsaurus_rs_table_usage";
const ROWS: usize = 100;
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("\ntable_usage failed: {e}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), ClientError> {
let client = Client::from_env()?;
let contacts = format!("{BASE}/contacts");
step("Preparing Cypress");
client.remove_tree(BASE)?;
client.create("map_node", BASE)?;
done(BASE);
step("Creating a table from the struct its rows have");
let schema = Contact::table_schema();
println!(" {}", render(&schema.to_yson()));
client.create_table(&contacts, &schema)?;
check(
"the cluster stored the struct's four columns",
column_names(&client.table_schema(&contacts)?) == ["name", "email", "phone", "age"],
)?;
step(&format!("Writing {ROWS} rows, as Rust values"));
client.write_table_rows(&contacts, (0..ROWS).map(contact))?;
done(&format!("{ROWS} contacts written"));
step("Asking the cluster how many rows it has");
let counted = client.row_count(&contacts)?;
check(&format!("row_count is {counted}"), counted == ROWS as i64)?;
let attrs: Attrs = client.get_as(&format!("{contacts}/@"))?;
check(
"and the attribute map agrees, read into a one-field struct",
attrs.row_count == counted,
)?;
step("Reading them back, as Rust values");
let read = client.read_table_rows::<Contact>(&contacts)?;
check(
&format!("{} rows came back", read.len()),
read.len() == ROWS,
)?;
let written: Vec<Contact> = (0..ROWS).map(contact).collect();
if let Some(n) = read
.iter()
.zip(&written)
.position(|(got, sent)| got != sent)
{
eprintln!(
" FAIL row {n} came back as {:?}, not {:?}",
read[n], written[n]
);
return Err(ClientError::Config(
"the round trip did not return what it was given".to_owned(),
));
}
check(
"and every one is the row that went in, in order",
read == written,
)?;
step("A struct naming one column is a projection");
let names = client.read_table_rows::<Name>(&contacts)?;
let first = names.first().map(|row| row.name.as_str());
check(
&format!("{} names came back, the first {first:?}", names.len()),
names.len() == ROWS && first == Some(written[0].name.as_str()),
)?;
step("The same projection, in the other direction");
let partial = format!("{BASE}/partial");
client.create_table(&partial, &Contact::table_schema())?;
match client.write_table_rows(&partial, names.iter().take(1)) {
Ok(()) => {
eprintln!(" FAIL a row with three columns missing was accepted");
return Err(ClientError::Config(
"the derived schema was not enforced".to_owned(),
));
}
Err(e) => {
let message = e.to_string();
check(
"a row missing the other three columns is refused",
message.contains("email") || message.to_lowercase().contains("required"),
)?;
println!(" {}", first_line(&message));
}
}
println!("\nA hundred Rust values in, and the same hundred out. Nothing here encodes");
println!("YSON: the schema came off the struct, and the cluster holds the rows to it.");
println!("A struct naming fewer columns reads them as a projection, and writes nothing.");
println!("Tables left at {BASE}");
Ok(())
}
#[derive(Serialize, Deserialize, PartialEq, Debug, ytsaurus_helpers::TableRow)]
struct Contact {
name: String,
email: String,
phone: String,
age: i64,
}
#[derive(Serialize, Deserialize)]
struct Name {
name: String,
}
#[derive(Deserialize)]
struct Attrs {
row_count: i64,
}
fn contact(n: usize) -> Contact {
Contact {
name: format!("Gopher {n}"),
email: format!("gopher{n}@ytsaurus.tech"),
phone: format!("+7{n:010}"),
age: 27 + (n % 40) as i64,
}
}
fn column_names(schema: &YsonValue) -> Vec<String> {
let YsonNode::List(columns) = &schema.node else {
return Vec::new();
};
columns
.iter()
.filter_map(|column| match &column.node {
YsonNode::Map(fields) => fields
.get(b"name".as_slice())
.and_then(|v| v.as_str())
.map(str::to_owned),
_ => None,
})
.collect()
}
fn render(value: &YsonValue) -> String {
to_string(value, YsonFormat::Text).unwrap_or_default()
}
fn first_line(message: &str) -> &str {
message.lines().next().unwrap_or(message)
}
fn step(what: &str) {
println!("\n== {what}");
}
fn done(what: &str) {
println!(" ok {what}");
}
fn check(what: &str, passed: bool) -> Result<(), ClientError> {
if passed {
done(what);
return Ok(());
}
eprintln!(" FAIL {what}");
Err(ClientError::Config(format!("check failed: {what}")))
}