use crate::error::{TViewError, TViewResult};
use crate::queue::affected::{self, Change, NetChange};
use crate::queue::published::{self, Listed, Publish};
use crate::utils::ident;
use crate::utils::spi::LookupKeys;
use serde_json::{Map, Value, json};
use std::collections::{BTreeSet, HashMap};
pub(crate) fn flush_and_report(
max_entities: i32,
include_data: bool,
reset: bool,
types: Option<Vec<String>>,
) -> TViewResult<Value> {
let wanted = types.map(requested_types).transpose()?;
crate::flush::flush_refresh_queue()?;
let capturing = crate::config::publish_changes() == Some(Publish::Rows);
let summary = affected::summarize(reset, capturing);
let limit = usize::try_from(max_entities).unwrap_or(0);
build_report(&summary, limit, include_data, wanted.as_ref())
}
const KNOWN_TYPES_LISTED: usize = 20;
fn requested_types(types: Vec<String>) -> TViewResult<BTreeSet<String>> {
let known: BTreeSet<String> = crate::catalog::row::typenames()?
.into_iter()
.map(|(_, typename)| typename)
.collect();
let unknown: Vec<&str> = types
.iter()
.filter(|t| !known.contains(*t))
.map(String::as_str)
.collect();
if !unknown.is_empty() {
let listed: Vec<&str> = known
.iter()
.take(KNOWN_TYPES_LISTED)
.map(String::as_str)
.collect();
let more = if known.len() > KNOWN_TYPES_LISTED {
", …"
} else {
""
};
return Err(TViewError::InvalidInput {
parameter: "types".to_string(),
reason: format!(
"unknown type(s) {}; known: {}{more}",
unknown.join(", "),
listed.join(", ")
),
});
}
Ok(types.into_iter().collect())
}
struct EntityInfo {
typename: String,
table: String,
}
fn entity_info(entities: &BTreeSet<&str>) -> TViewResult<HashMap<String, EntityInfo>> {
let names: Vec<String> = entities.iter().map(|e| (*e).to_string()).collect();
Ok(crate::catalog::queries::typenames_and_tables(names)?
.into_iter()
.map(|(entity, typename, table)| (entity, EntityInfo { typename, table }))
.collect())
}
fn current_data(entity: &str, table: &str, pks: Vec<i64>) -> TViewResult<HashMap<i64, Value>> {
let Some(lookup) = LookupKeys::of(pks) else {
return Ok(HashMap::new());
};
let qi_pk = ident::quoted(&format!("pk_{entity}"));
let sql = format!(
"SELECT t.{qi_pk}, t.data FROM {table} t WHERE {}",
lookup.predicate(&format!("t.{qi_pk}"), 1)
);
let rows = crate::utils::spi::kept_rows_as(&sql, &[lookup.arg()], lookup.plan(), |row| {
Ok((row.get::<i64>(1)?, row.get::<pgrx::JsonB>(2)?))
})?;
Ok(rows
.into_iter()
.filter_map(|(pk, data)| Some((pk?, data.map_or(Value::Null, |d| d.0))))
.collect())
}
fn build_report(
summary: &affected::Summary,
max_entities: usize,
include_data: bool,
wanted: Option<&BTreeSet<String>>,
) -> TViewResult<Value> {
let (changes, overflow) = (&summary.changes, &summary.overflow);
let entities: BTreeSet<&str> = changes
.iter()
.map(|c| c.entity.as_str())
.chain(overflow.iter().map(String::as_str))
.collect();
let info = entity_info(&entities)?;
let typename = |entity: &str| {
info.get(entity).map_or_else(
|| crate::catalog::typename(entity, None),
|i| i.typename.clone(),
)
};
let asked = |entity: &str| wanted.is_none_or(|w| w.contains(&typename(entity)));
let omitted: BTreeSet<String> = changes
.iter()
.map(|c| c.entity.as_str())
.chain(overflow.iter().map(String::as_str))
.filter(|entity| !asked(entity))
.map(typename)
.collect();
let listed = |c: &&NetChange| include_data || !overflow.contains(&c.entity);
let mut changes: Vec<NetChange> = changes
.iter()
.filter(|c| asked(&c.entity))
.filter(listed)
.cloned()
.collect();
changes.sort_by(|a, b| (&a.entity, a.pk).cmp(&(&b.entity, b.pk)));
let (reported, left_out) = changes.split_at(changes.len().min(max_entities));
let mut rows: HashMap<(String, i64), Value> = HashMap::new();
if include_data {
let mut pks_by_entity: HashMap<&str, Vec<i64>> = HashMap::new();
for c in reported.iter().filter(|c| c.change != Change::Deleted) {
pks_by_entity.entry(&c.entity).or_default().push(c.pk);
}
for (entity, pks) in pks_by_entity {
if let Some(i) = info.get(entity) {
for (pk, data) in current_data(entity, &i.table, pks)? {
rows.insert((entity.to_string(), pk), data);
}
}
}
}
let mut updated = Vec::new();
let mut deleted = Vec::new();
for c in reported {
let mut item = Map::new();
item.insert("__typename".into(), Value::String(typename(&c.entity)));
item.insert(
"id".into(),
c.id.as_ref()
.map_or(Value::Null, crate::catalog::RowId::value),
);
if c.change == Change::Deleted {
deleted.push(Value::Object(item));
continue;
}
let op = if c.change == Change::Inserted {
"CREATED"
} else {
"UPDATED"
};
item.insert("operation".into(), Value::String(op.into()));
if include_data {
let Some(data) = rows.remove(&(c.entity.clone(), c.pk)) else {
continue;
};
item.insert("data".into(), data);
}
updated.push(Value::Object(item));
}
let mut invalidated: BTreeSet<String> = left_out
.iter()
.map(|c| c.entity.as_str())
.chain(overflow.iter().map(String::as_str).filter(|e| asked(e)))
.map(typename)
.collect();
let collections = summary
.parents
.as_deref()
.map(|parents| collections(parents, wanted, &mut invalidated))
.transpose()?;
let mut report = json!({
"updated": updated,
"deleted": deleted,
"truncated": !invalidated.is_empty(),
"invalidated_types": invalidated.into_iter().collect::<Vec<_>>(),
});
if let Some(collections) = collections {
report["collections"] = collections;
}
if wanted.is_some() {
report["omitted_types"] = json!(omitted.into_iter().collect::<Vec<_>>());
}
Ok(report)
}
fn collections(
parents: &[(
std::rc::Rc<crate::catalog::Collection>,
crate::catalog::collections::ParentRef,
)],
wanted: Option<&BTreeSet<String>>,
invalidated: &mut BTreeSet<String>,
) -> TViewResult<Value> {
let asked: Vec<_> = parents
.iter()
.filter(|(c, _)| wanted.is_none_or(|w| w.contains(&*c.child_type)))
.cloned()
.collect();
let mut ids: std::collections::BTreeMap<&str, (&str, BTreeSet<String>)> =
std::collections::BTreeMap::new();
for ((collection, _), listed) in asked.iter().zip(published::listed(&asked)) {
let child = &*collection.child_type;
match listed {
Listed::Id(id) => {
ids.entry(&collection.key)
.or_insert_with(|| (child, BTreeSet::new()))
.1
.insert(id);
}
Listed::Gone => {}
Listed::Unlisted => {
invalidated.insert(child.to_string());
}
}
}
let mut out = Map::new();
for (key, (child, ids)) in ids {
if invalidated.contains(child) {
continue;
}
let values = ids
.iter()
.map(|id| serde_json::from_str(id))
.collect::<Result<Vec<Value>, _>>()
.map_err(|e| TViewError::SerializationError {
message: format!("{key}: unreadable parent id: {e}"),
})?;
out.insert(key.to_string(), Value::Array(values));
}
Ok(Value::Object(out))
}