use std::collections::{BTreeMap, BTreeSet};
use perspective_client::config::ViewConfig;
use perspective_client::{DeleteOptions, Table, View};
use perspective_js::utils::*;
use crate::config::DerivedTableConfig;
use crate::session::ViewSubscription;
use crate::utils::PtrEqRc;
pub enum DerivedSource {
Panel(ViewSubscription),
Restored(View),
}
impl DerivedSource {
async fn delete(self) -> ApiResult<()> {
match self {
Self::Panel(sub) => sub.delete().await,
Self::Restored(view) => Ok(view.delete().await?),
}
}
}
pub struct DerivedTable {
parent: String,
view_config: ViewConfig,
source: DerivedSource,
table: Table,
}
impl DerivedTable {
pub fn new(
parent: String,
view_config: ViewConfig,
source: DerivedSource,
table: Table,
) -> Self {
Self {
parent,
view_config,
source,
table,
}
}
pub fn parent(&self) -> &str {
&self.parent
}
pub fn view_config(&self) -> &ViewConfig {
&self.view_config
}
pub fn table(&self) -> &Table {
&self.table
}
pub fn config(&self) -> DerivedTableConfig {
DerivedTableConfig {
table: self.parent.clone(),
view_config: self.view_config.clone(),
}
}
pub fn matches(&self, config: &DerivedTableConfig) -> bool {
self.parent == config.table && self.view_config == config.view_config
}
pub async fn delete(self) -> ApiResult<()> {
self.table.delete(DeleteOptions::default()).await?;
self.source.delete().await
}
}
#[derive(Default)]
pub struct DerivedRegistry {
live: BTreeMap<String, DerivedTable>,
pending: BTreeMap<String, DerivedTableConfig>,
}
impl DerivedRegistry {
pub fn get(&self, name: &str) -> Option<&DerivedTable> {
self.live.get(name)
}
pub fn contains(&self, name: &str) -> bool {
self.live.contains_key(name)
}
pub fn insert(&mut self, name: String, table: DerivedTable) {
self.pending.remove(&name);
self.live.insert(name, table);
}
pub fn remove(&mut self, name: &str) -> Option<DerivedTable> {
self.pending.remove(name);
self.live.remove(name)
}
pub fn names(&self) -> Vec<String> {
self.live.keys().cloned().collect()
}
pub fn find_structural(&self, config: &DerivedTableConfig) -> Option<String> {
self.live
.iter()
.find(|(_, table)| table.matches(config))
.map(|(name, _)| name.clone())
}
pub fn pending(&self) -> BTreeMap<String, DerivedTableConfig> {
self.pending.clone()
}
pub fn set_pending(&mut self, pending: BTreeMap<String, DerivedTableConfig>) {
self.pending = pending;
}
pub fn configs(&self) -> BTreeMap<String, DerivedTableConfig> {
let mut out = self.pending.clone();
out.extend(
self.live
.iter()
.map(|(name, table)| (name.clone(), table.config())),
);
out
}
pub fn children(&self, name: &str) -> Vec<String> {
self.live
.iter()
.filter(|(_, table)| table.parent == name)
.map(|(child, _)| child.clone())
.chain(
self.pending
.iter()
.filter(|(_, config)| config.table == name)
.map(|(child, _)| child.clone()),
)
.collect()
}
pub fn ancestors(&self, name: &str) -> Vec<String> {
let mut out = vec![];
let mut seen = BTreeSet::new();
let mut cursor = name.to_owned();
while let Some(entry) = self.live.get(&cursor) {
if !seen.insert(cursor.clone()) {
break;
}
out.push(entry.parent.clone());
cursor = entry.parent.clone();
}
out
}
}
pub fn topo_order(configs: &BTreeMap<String, DerivedTableConfig>) -> Result<Vec<String>, String> {
let mut pending = configs.keys().cloned().collect::<BTreeSet<_>>();
let mut out = Vec::with_capacity(configs.len());
while !pending.is_empty() {
let ready = pending
.iter()
.filter(|name| {
let parent = &configs[*name].table;
!pending.contains(parent)
})
.cloned()
.collect::<Vec<_>>();
if ready.is_empty() {
let cycle = pending.into_iter().collect::<Vec<_>>().join("`, `");
return Err(format!(
"`derived_tables` has a dependency cycle among `{cycle}`"
));
}
for name in ready {
pending.remove(&name);
out.push(name);
}
}
Ok(out)
}
pub fn reusable(
order: &[String],
configs: &BTreeMap<String, DerivedTableConfig>,
registry: &DerivedRegistry,
) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for name in order {
let config = &configs[name];
let parent_ok = !configs.contains_key(&config.table) || out.contains(&config.table);
if parent_ok
&& registry
.get(name)
.is_some_and(|table| table.matches(config))
{
out.insert(name.clone());
}
}
out
}
pub fn suggest_name(
taken: &BTreeSet<String>,
config: &PtrEqRc<ViewConfig>,
table: &Option<String>,
) -> String {
let table_name = table.as_deref().unwrap_or("...");
let name = if config.group_by.is_empty() {
table_name.to_owned()
} else {
format!("{} GROUP BY {}", table_name, config.group_by.join(", "))
};
if taken.contains(&name) {
(1..)
.map(|n| format!("{name} {n}"))
.find(|name| !taken.contains(name))
.unwrap()
} else {
name
}
}