Skip to content

Commit ee75625

Browse files
authored
Merge pull request #15 from pbudzik/feat/rollup-input-provenance
Rollup → raw segment provenance (closes spec §19.10)
2 parents 215cdf3 + 76bb382 commit ee75625

5 files changed

Lines changed: 306 additions & 15 deletions

File tree

src/api/http_server.rs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -526,22 +526,35 @@ async fn handle_explain(
526526
let corrections = execute_plan(&state, &plan_corr).await;
527527

528528
// Segment provenance from the manifest. Filtering by bucket here
529-
// matches the executor's pruning — the same segments the query would
530-
// open are the ones we report.
531-
let (watermark_ms, rollup_segments, raw_segments) = {
529+
// matches the executor's pruning. For each overlapping rollup
530+
// segment we also surface its `input_segment_ids` so an operator
531+
// can name every raw segment that contributed to a rollup line —
532+
// spec §19.10 (invoice snapshots reference a watermark + source
533+
// segment set).
534+
let (watermark_ms, rollup_segments, rollup_inputs, raw_segments) = {
532535
let manifest = state.manifest.read().await;
533536
let bucket_count = manifest.bucket_count.max(1);
534537
let target_bucket = bucket_for_account(&AccountId(account_id.clone()), bucket_count);
535-
let rollups: Vec<String> = manifest
536-
.rollup_segments
537-
.iter()
538-
.filter(|s| {
539-
s.bucket == target_bucket
540-
&& s.min_timestamp_ms < to_ms
541-
&& s.max_timestamp_ms >= from_ms
542-
})
543-
.map(|s| s.segment_id.clone())
544-
.collect();
538+
let mut rollup_ids = Vec::new();
539+
let mut inputs_map = serde_json::Map::new();
540+
for s in &manifest.rollup_segments {
541+
if s.bucket != target_bucket
542+
|| s.min_timestamp_ms >= to_ms
543+
|| s.max_timestamp_ms < from_ms
544+
{
545+
continue;
546+
}
547+
rollup_ids.push(s.segment_id.clone());
548+
inputs_map.insert(
549+
s.segment_id.clone(),
550+
serde_json::Value::Array(
551+
s.input_segment_ids
552+
.iter()
553+
.map(|id| serde_json::Value::String(id.clone()))
554+
.collect(),
555+
),
556+
);
557+
}
545558
let raws: Vec<String> = manifest
546559
.raw_segments
547560
.iter()
@@ -552,7 +565,12 @@ async fn handle_explain(
552565
})
553566
.map(|s| s.segment_id.clone())
554567
.collect();
555-
(manifest.watermarks.hourly_rollup_ms, rollups, raws)
568+
(
569+
manifest.watermarks.hourly_rollup_ms,
570+
rollup_ids,
571+
inputs_map,
572+
raws,
573+
)
556574
};
557575

558576
Ok(Json(serde_json::json!({
@@ -562,6 +580,7 @@ async fn handle_explain(
562580
"watermark_ms": watermark_ms,
563581
"lines": lines,
564582
"rollup_segments": rollup_segments,
583+
"rollup_inputs": rollup_inputs,
565584
"raw_segments": raw_segments,
566585
"corrections": corrections,
567586
})))

src/ingest/flusher.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,5 +239,9 @@ pub fn build_segment_meta(segment_id: &str, batch: &[UsageEvent], bucket: u32, c
239239
model_ids,
240240
quantity_sum: Some(quantity_sum),
241241
checksum,
242+
// Raw segments have no inputs — they're the ground truth.
243+
// Compacted segments also leave this empty; their provenance is
244+
// tracked via Manifest.compacted_replacements.
245+
input_segment_ids: Vec::new(),
242246
}
243247
}

src/rollup/worker.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@ impl RollupWorker {
202202
let hour_end = hour + HOUR_MS;
203203

204204
let mut by_bucket: HashMap<u32, RollupBuilder> = HashMap::new();
205+
// Track per-bucket provenance: which raw segment IDs
206+
// contributed events to each output rollup segment. Used to
207+
// populate `SegmentMeta.input_segment_ids` so invoice
208+
// lineage is auditable (spec §19.10).
209+
let mut inputs_by_bucket: HashMap<u32, std::collections::BTreeSet<String>> =
210+
HashMap::new();
211+
205212
for seg in &raw_segments {
206213
if seg.max_timestamp_ms < hour || seg.min_timestamp_ms >= hour_end {
207214
continue;
@@ -225,6 +232,10 @@ impl RollupWorker {
225232
continue;
226233
}
227234
by_bucket.entry(bucket).or_default().process_event(&event);
235+
inputs_by_bucket
236+
.entry(bucket)
237+
.or_default()
238+
.insert(seg.segment_id.clone());
228239
}
229240
}
230241

@@ -233,7 +244,12 @@ impl RollupWorker {
233244
if records.is_empty() {
234245
continue;
235246
}
236-
let (meta, path) = self.write_rollup_segment(records, bucket, hour, hour_end)?;
247+
let inputs: Vec<String> = inputs_by_bucket
248+
.remove(&bucket)
249+
.map(|set| set.into_iter().collect())
250+
.unwrap_or_default();
251+
let (meta, path) =
252+
self.write_rollup_segment(records, bucket, hour, hour_end, inputs)?;
237253
new_segments.push((meta, path));
238254
}
239255

@@ -331,6 +347,7 @@ impl RollupWorker {
331347
bucket: u32,
332348
hour_start: i64,
333349
hour_end: i64,
350+
input_segment_ids: Vec<String>,
334351
) -> anyhow::Result<(SegmentMeta, std::path::PathBuf)> {
335352
let segment_id = format!("rollup_{}", uuid::Uuid::new_v4().simple());
336353
let path = self
@@ -353,6 +370,7 @@ impl RollupWorker {
353370
hour_start,
354371
hour_end - 1,
355372
row_count,
373+
input_segment_ids,
356374
);
357375
Ok((meta, path))
358376
}
@@ -366,6 +384,7 @@ fn build_rollup_segment_meta(
366384
min_ts: i64,
367385
max_ts: i64,
368386
row_count: u64,
387+
input_segment_ids: Vec<String>,
369388
) -> SegmentMeta {
370389
let mut product_ids = std::collections::HashSet::new();
371390
let mut meter_ids = std::collections::HashSet::new();
@@ -408,6 +427,7 @@ fn build_rollup_segment_meta(
408427
model_ids,
409428
quantity_sum: Some(quantity_sum),
410429
checksum,
430+
input_segment_ids,
411431
}
412432
}
413433

src/storage/manifest.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ pub struct SegmentMeta {
2626
pub model_ids: HashSet<ModelId>,
2727
pub quantity_sum: Option<i128>,
2828
pub checksum: u64,
29+
/// For rollup segments: the raw segment IDs whose events were
30+
/// aggregated to produce this rollup. Empty for raw and compacted
31+
/// segments (compacted segments' provenance lives in
32+
/// `Manifest.compacted_replacements`).
33+
///
34+
/// Spec §19.10: invoice snapshots must reference a watermark + the
35+
/// source segment set. This is the per-rollup half of that — given
36+
/// a rollup segment, you can name every raw segment that
37+
/// contributed to it, so an invoice line's lineage is auditable.
38+
#[serde(default)]
39+
pub input_segment_ids: Vec<String>,
2940
}
3041

3142
#[derive(Debug, Clone, Serialize, Deserialize)]

0 commit comments

Comments
 (0)