use std::time::Instant;
use serde_json::{Value, json};
use super::options::{Options, directory_bytes};
use super::served::Writer;
use super::{ProbeResult, Table};
const WORDS: [&str; 32] = [
"troponin",
"patient",
"ward",
"review",
"blood",
"pressure",
"pulse",
"oxygen",
"chest",
"pain",
"stable",
"monitor",
"nurse",
"doctor",
"test",
"result",
"morning",
"evening",
"normal",
"repeat",
"care",
"plan",
"heart",
"rhythm",
"temperature",
"fluid",
"oral",
"dose",
"follow",
"observe",
"sample",
"rest",
];
const STATUSES: [&str; 3] = ["open", "closed", "pending"];
pub const TEXT_SEED: u64 = 0x4841_454d_5445_5854;
pub const VECTOR_SEED: u64 = 0x4841_454d_5645_4354;
pub fn note_id(index: u32) -> String {
format!("n{index:010}")
}
fn patient_id(index: u32) -> String {
format!("p{index:010}")
}
fn ward_id(index: u32) -> String {
format!("w{index:02}")
}
pub fn build(
writer: &mut Writer,
options: &Options,
directory: &std::path::Path,
table: &mut Table,
) -> ProbeResult<()> {
collections(writer, table)?;
documents(writer, options, table)?;
relations(writer, options.records, table)?;
vectors(writer, options, table)?;
let started = Instant::now();
let bytes = directory_bytes(&directory.join("database"))?;
let elapsed = started.elapsed();
table.size(
"store logical file bytes after build",
elapsed,
bytes.logical,
"Rust metadata walk; file lengths; hard links counted once; excludes config and socket",
)?;
table.size(
"store allocated bytes after build",
elapsed,
bytes.allocated,
"same Rust walk; Unix 512-byte blocks including directories; hard links counted once",
)?;
table.metadata("query head", &serde_json::to_string(&writer.head)?)?;
Ok(())
}
fn collections(writer: &mut Writer, table: &mut Table) -> ProbeResult<()> {
let started = Instant::now();
for collection in ["patients", "notes", "wards"] {
writer.apply(
"collection.put",
&json!({"collection": collection, "schema": null}),
)?;
}
table.measured(
"collections declared",
started.elapsed(),
3,
"collection.put; durable replies",
)?;
Ok(())
}
fn documents(writer: &mut Writer, options: &Options, table: &mut Table) -> ProbeResult<()> {
let patients = options.records / 10;
let started = Instant::now();
put_batches(writer, "wards", 20, |index| {
Ok(json!({"id": ward_id(index)}))
})?;
put_batches(writer, "patients", patients, |index| {
Ok(json!({"id": patient_id(index), "ward": ward_id(index % 20)}))
})?;
table.measured(
"patients and wards written",
started.elapsed(),
patients + 20,
"documents.put_many; batches of at most 1000",
)?;
let mut text = Generator::new(TEXT_SEED);
let mut hits = 0_u32;
let started = Instant::now();
put_batches(writer, "notes", options.records, |index| {
let body = text.body(index)?;
if body.split_whitespace().any(|word| word == "troponin") {
hits = hits
.checked_add(1)
.ok_or("troponin hit count exceeds u32")?;
}
let patient = index % patients;
let status = STATUSES
.get(usize::try_from(index % 3)?)
.ok_or("status index is outside the fixture vocabulary")?;
Ok(json!({
"id": note_id(index), "patient_id": patient_id(patient),
"patient": {"id": patient_id(patient)}, "ward": ward_id(patient % 20),
"status": status, "body": body, "written": index
}))
})?;
table.measured(
"notes written",
started.elapsed(),
options.records,
"documents.put_many; batches of at most 1000; includes generation and encoding",
)?;
table.metadata("fixture troponin hits", &hits.to_string())?;
Ok(())
}
fn relations(writer: &mut Writer, records: u32, table: &mut Table) -> ProbeResult<()> {
let patients = records / 10;
let started = Instant::now();
for (relation, from, to) in [("about", "notes", "patients"), ("in", "patients", "wards")] {
writer.apply(
"relation.put",
&json!({"relation": relation, "from_collection": from,
"to_collection": to, "cardinality": "many_to_many"}),
)?;
}
table.measured(
"relations declared",
started.elapsed(),
2,
"relation.put declares kinds; edge.put writes each member",
)?;
let started = Instant::now();
for index in 0..records {
edge(
writer,
"about",
"notes",
¬e_id(index),
"patients",
&patient_id(index % patients),
)?;
}
for index in 0..patients {
edge(
writer,
"in",
"patients",
&patient_id(index),
"wards",
&ward_id(index % 20),
)?;
}
let relations = records
.checked_add(patients)
.ok_or("relation count exceeds u32")?;
table.measured(
"relations written",
started.elapsed(),
relations,
"edge.put; one durable mutation per edge",
)?;
Ok(())
}
fn vectors(writer: &mut Writer, options: &Options, table: &mut Table) -> ProbeResult<()> {
let mut vectors = Generator::new(VECTOR_SEED);
let started = Instant::now();
for index in 0..options.records {
writer.apply(
"vector.put",
&json!({"collection": "notes", "document_id": note_id(index),
"field": "body_vec", "vector": vectors.unit_vector(options.dims)?}),
)?;
}
table.measured(
"vectors written",
started.elapsed(),
options.records,
"vector.put; seeded unit vectors; includes generation and encoding",
)?;
table.metadata("vector declaration", &format!(
"notes.body_vec: supplied, {} dimensions, fixed by first vector.put; vector.declare is embedded-only",
options.dims))?;
Ok(())
}
fn put_batches(
writer: &mut Writer,
collection: &str,
count: u32,
mut document: impl FnMut(u32) -> ProbeResult<Value>,
) -> ProbeResult<()> {
let mut start = 0_u32;
while start < count {
let end = start.saturating_add(1000).min(count);
let mut documents = Vec::new();
for index in start..end {
let id = match collection {
"notes" => note_id(index),
"patients" => patient_id(index),
"wards" => ward_id(index),
_ => return Err("unknown fixture collection".into()),
};
documents.push(json!({"document_id": id, "document": document(index)?}));
}
writer.apply(
"documents.put_many",
&json!({"collection": collection, "documents": documents}),
)?;
start = end;
}
Ok(())
}
fn edge(
writer: &mut Writer,
relation: &str,
from_collection: &str,
from: &str,
to_collection: &str,
to: &str,
) -> ProbeResult<()> {
writer.apply(
"edge.put",
&json!({"relation": relation,
"from": {"collection": from_collection, "document_id": from},
"to": {"collection": to_collection, "document_id": to}, "properties": {}}),
)
}
struct Generator {
state: u64,
}
impl Generator {
const fn new(state: u64) -> Self {
Self { state }
}
const fn next(&mut self) -> u64 {
self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut value = self.state;
value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
value ^ (value >> 31)
}
fn below(&mut self, bound: usize) -> ProbeResult<usize> {
if bound == 0 {
return Err("generator bound must be positive".into());
}
Ok(usize::try_from(self.next() % u64::try_from(bound)?)?)
}
fn body(&mut self, index: u32) -> ProbeResult<String> {
let count = 20 + self.below(41)?;
let mut words = Vec::with_capacity(count);
for position in 0..count {
let word = if position == 0 && index.is_multiple_of(10) {
"troponin"
} else {
WORDS
.get(self.below(WORDS.len())?)
.copied()
.ok_or("word index is outside the fixture vocabulary")?
};
words.push(word);
}
Ok(words.join(" "))
}
fn unit_vector(&mut self, dims: usize) -> ProbeResult<Vec<f32>> {
let mut vector = Vec::new();
vector.try_reserve_exact(dims)?;
for _ in 0..dims {
let bits = u16::try_from(self.next() >> 48)?;
vector.push(f32::from(bits) / 32_767.5 - 1.0);
}
let norm = vector
.iter()
.map(|value| f64::from(*value).powi(2))
.sum::<f64>()
.sqrt();
if !norm.is_finite() || norm <= 0.0 {
return Err("seeded vector has no finite positive norm".into());
}
for value in &mut vector {
*value = num_traits::ToPrimitive::to_f32(&(f64::from(*value) / norm))
.ok_or("normalized component is not representable as f32")?;
}
Ok(vector)
}
}