use std::collections::{HashMap, HashSet, VecDeque};
use postcard::from_bytes;
use reifydb_catalog::catalog::Catalog;
use reifydb_core::{
error::diagnostic::catalog::update_partition_column_immutable,
interface::catalog::id::{RingBufferId, SeriesId, TableId, ViewId},
internal_error,
};
use reifydb_transaction::transaction::Transaction;
use reifydb_value::return_error;
use crate::{Result, expression::IdentExpression, flow::operator::OperatorDef};
pub(crate) enum UpdateTarget {
Table(TableId),
RingBuffer(RingBufferId),
Series(SeriesId),
}
impl UpdateTarget {
fn matches_source(&self, ty: &OperatorDef) -> bool {
match (self, ty) {
(
UpdateTarget::Table(id),
OperatorDef::SourceTable {
table,
..
},
) => table == id,
(
UpdateTarget::RingBuffer(id),
OperatorDef::SourceRingBuffer {
ringbuffer,
..
},
) => ringbuffer == id,
(
UpdateTarget::Series(id),
OperatorDef::SourceSeries {
series,
..
},
) => series == id,
_ => false,
}
}
}
pub(crate) fn check_partition_immutability(
catalog: &Catalog,
tx: &mut Transaction<'_>,
target_namespace: &str,
target_name: &str,
target: UpdateTarget,
assigned_columns: &[IdentExpression],
) -> Result<()> {
if assigned_columns.is_empty() {
return Ok(());
}
let forbidden = downstream_view_partition_columns(catalog, tx, target)?;
if forbidden.is_empty() {
return Ok(());
}
for assigned in assigned_columns {
let column_name = assigned.0.text();
let Some(view_id) = forbidden.get(column_name) else {
continue;
};
let view = catalog.get_view(tx, *view_id)?;
let view_namespace = catalog
.find_namespace(tx, view.namespace())?
.map(|n| n.name().to_string())
.unwrap_or_else(|| "?".to_string());
return_error!(update_partition_column_immutable(
assigned.0.clone(),
column_name,
target_namespace,
target_name,
Some((view_namespace.as_str(), view.name())),
));
}
Ok(())
}
fn downstream_view_partition_columns(
catalog: &Catalog,
tx: &mut Transaction<'_>,
target: UpdateTarget,
) -> Result<HashMap<String, ViewId>> {
let mut forbidden: HashMap<String, ViewId> = HashMap::new();
let nodes = catalog.list_operators_all(tx)?;
let mut decoded = Vec::with_capacity(nodes.len());
for node in &nodes {
let ty: OperatorDef = from_bytes(node.data.as_ref())
.map_err(|e| internal_error!("Failed to deserialize flow node type: {}", e))?;
decoded.push((node.flow, ty));
}
let mut worklist: VecDeque<Anchor> = VecDeque::from([Anchor::Target(target)]);
let mut visited_views: HashSet<ViewId> = HashSet::new();
while let Some(anchor) = worklist.pop_front() {
let matched_flows: HashSet<_> =
decoded.iter().filter(|(_, ty)| anchor.matches_source(ty)).map(|(flow, _)| *flow).collect();
for flow_id in matched_flows {
for (node_flow, ty) in &decoded {
if *node_flow != flow_id {
continue;
}
let view = match ty {
OperatorDef::SinkTableView {
view,
}
| OperatorDef::SinkRingBufferView {
view,
..
}
| OperatorDef::SinkSeriesView {
view,
..
} => *view,
_ => continue,
};
let partition_by = catalog.get_view(tx, view)?.partition_by().to_vec();
for column in partition_by {
forbidden.entry(column).or_insert(view);
}
if visited_views.insert(view) {
worklist.push_back(Anchor::View(view));
}
}
}
}
Ok(forbidden)
}
enum Anchor {
Target(UpdateTarget),
View(ViewId),
}
impl Anchor {
fn matches_source(&self, ty: &OperatorDef) -> bool {
match self {
Anchor::Target(target) => target.matches_source(ty),
Anchor::View(id) => matches!(ty, OperatorDef::SourceView { view } if view == id),
}
}
}