Phase 10: Rich catalog v2 — metadata as product
- DatasetManifest expanded: description, owner, sensitivity, columns,
lineage, freshness contract, tags, row_count
- All new fields use #[serde(default)] for backward compatibility
- PII auto-detection: scans column names for email, phone, SSN, salary,
address, DOB, medical terms — flags as PII/PHI/Financial
- Column-level metadata: name, type, sensitivity, is_pii flag
- Lineage tracking: source_system, source_file, ingest_job, timestamp
- Ingest pipeline auto-populates: PII scan, column meta, lineage, row count
- PATCH /catalog/datasets/by-name/{name}/metadata — update metadata
- Catalog responses now include all rich fields
- 25 unit tests passing (5 new PII detection tests)
Per ADR-013: datasets without metadata become mystery files.
This makes every ingested file self-describing from day one.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
use shared::types::{DatasetId, DatasetManifest, ObjectRef, SchemaFingerprint};
|
||||
use shared::types::{
|
||||
DatasetId, DatasetManifest, ObjectRef, SchemaFingerprint,
|
||||
ColumnMeta, Lineage, FreshnessContract, Sensitivity,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
@@ -6,6 +9,19 @@ use tokio::sync::RwLock;
|
||||
use storaged::ops;
|
||||
use object_store::ObjectStore;
|
||||
|
||||
/// Partial metadata update — only set fields are applied.
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize)]
|
||||
pub struct MetadataUpdate {
|
||||
pub description: Option<String>,
|
||||
pub owner: Option<String>,
|
||||
pub sensitivity: Option<Sensitivity>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub columns: Option<Vec<ColumnMeta>>,
|
||||
pub lineage: Option<Lineage>,
|
||||
pub freshness: Option<FreshnessContract>,
|
||||
pub row_count: Option<u64>,
|
||||
}
|
||||
|
||||
const MANIFEST_PREFIX: &str = "_catalog/manifests";
|
||||
|
||||
/// In-memory dataset registry backed by manifest persistence in object storage.
|
||||
@@ -54,6 +70,14 @@ impl Registry {
|
||||
objects,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
description: String::new(),
|
||||
owner: String::new(),
|
||||
sensitivity: None,
|
||||
columns: vec![],
|
||||
lineage: None,
|
||||
freshness: None,
|
||||
tags: vec![],
|
||||
row_count: None,
|
||||
};
|
||||
|
||||
// Write-ahead: persist before in-memory update
|
||||
@@ -68,6 +92,36 @@ impl Registry {
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
/// Update metadata on an existing dataset (owner, description, tags, sensitivity, etc.)
|
||||
pub async fn update_metadata(
|
||||
&self,
|
||||
name: &str,
|
||||
updates: MetadataUpdate,
|
||||
) -> Result<DatasetManifest, String> {
|
||||
let mut datasets = self.datasets.write().await;
|
||||
let manifest = datasets.values_mut()
|
||||
.find(|d| d.name == name)
|
||||
.ok_or_else(|| format!("dataset not found: {name}"))?;
|
||||
|
||||
if let Some(desc) = updates.description { manifest.description = desc; }
|
||||
if let Some(owner) = updates.owner { manifest.owner = owner; }
|
||||
if let Some(sens) = updates.sensitivity { manifest.sensitivity = Some(sens); }
|
||||
if let Some(tags) = updates.tags { manifest.tags = tags; }
|
||||
if let Some(cols) = updates.columns { manifest.columns = cols; }
|
||||
if let Some(lineage) = updates.lineage { manifest.lineage = Some(lineage); }
|
||||
if let Some(freshness) = updates.freshness { manifest.freshness = Some(freshness); }
|
||||
if let Some(count) = updates.row_count { manifest.row_count = Some(count); }
|
||||
manifest.updated_at = chrono::Utc::now();
|
||||
|
||||
// Persist
|
||||
let manifest_key = format!("{MANIFEST_PREFIX}/{}.json", manifest.id);
|
||||
let json = serde_json::to_vec_pretty(manifest).map_err(|e| e.to_string())?;
|
||||
ops::put(&self.store, &manifest_key, json.into()).await?;
|
||||
|
||||
let result = manifest.clone();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get a dataset by ID.
|
||||
pub async fn get(&self, id: &DatasetId) -> Option<DatasetManifest> {
|
||||
let datasets = self.datasets.read().await;
|
||||
|
||||
Reference in New Issue
Block a user