use super::{Column, QueryGraph, walk};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdentityKind {
Pk,
DistinctOn,
}
impl IdentityKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Pk => "pk",
Self::DistinctOn => "distinct_on",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputColumn {
pub name: String,
pub junk: bool,
pub sortgroupref: u32,
pub column: Option<Column>,
pub type_oid: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SelectedIdentity {
pub position: usize,
pub kind: IdentityKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdentityError {
Missing,
Composite,
Unprojected,
NotAColumn,
}
pub fn select_identity(
entity: &str,
outputs: &[OutputColumn],
distinct_on: Option<&[u32]>,
equal: &dyn Fn(&Column, &Column) -> bool,
) -> Result<SelectedIdentity, IdentityError> {
let Some(refs) = distinct_on else {
let key = format!("pk_{entity}");
return outputs
.iter()
.position(|o| !o.junk && o.name == key)
.map(|position| SelectedIdentity {
position,
kind: IdentityKind::Pk,
})
.ok_or(IdentityError::Missing);
};
let [sortgroupref] = refs else {
return Err(IdentityError::Composite);
};
let Some(key) = outputs.iter().position(|o| o.sortgroupref == *sortgroupref) else {
return Err(IdentityError::Unprojected);
};
let chosen = |position| {
Ok(SelectedIdentity {
position,
kind: IdentityKind::DistinctOn,
})
};
match (&outputs[key], outputs[key].junk) {
(o, false) if o.column.is_some() => chosen(key),
(_, false) => Err(IdentityError::NotAColumn),
(
OutputColumn {
column: Some(column),
..
},
true,
) => outputs
.iter()
.position(|o| {
!o.junk
&& o.column
.as_ref()
.is_some_and(|c| c == column || equal(c, column))
})
.map_or(Err(IdentityError::Unprojected), chosen),
(_, true) => Err(IdentityError::Unprojected),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Identity {
pub name: String,
pub type_oid: u32,
pub kind: IdentityKind,
pub columns: Vec<(u32, i16)>,
}
#[must_use]
pub fn distinct_on_list(viewdef: &str) -> Vec<String> {
let Some(start) = viewdef.find("DISTINCT ON (") else {
return Vec::new();
};
let mut items = Vec::new();
let mut depth = 0_usize;
let mut quote: Option<char> = None;
let mut current = String::new();
for ch in viewdef[start + "DISTINCT ON (".len()..].chars() {
match (quote, ch) {
(Some(q), c) if c == q => quote = None,
(Some(_), _) => {}
(None, '\'' | '"') => quote = Some(ch),
(None, '(') => depth += 1,
(None, ')') if depth == 0 => {
items.push(current.trim().to_string());
return items;
}
(None, ')') => depth -= 1,
(None, ',') if depth == 0 => {
items.push(current.trim().to_string());
current.clear();
continue;
}
_ => {}
}
current.push(ch);
}
Vec::new()
}
#[must_use]
pub fn identity_refusal(entity: &str, error: IdentityError, keys: &[String]) -> String {
let key = keys.join(", ");
match error {
IdentityError::Missing => format!("tv_{entity} has no pk_{entity} output column"),
IdentityError::Composite => format!(
"tv_{entity} has a composite DISTINCT ON key ({key}): a TVIEW row is one entity, \
addressed by one key (pk_<entity>, id or identifier), and its parents embed it \
through one fk_<entity>. Model one row per ({key}) as an entity of its own, a \
tb_<entity> table with its pk_<entity>, or DISTINCT ON one column"
),
IdentityError::Unprojected => format!(
"the DISTINCT ON key of tv_{entity} ({key}) names its rows, but it is not an output \
column and no output column equals it: project it (… AS <name>)"
),
IdentityError::NotAColumn => format!(
"the DISTINCT ON key of tv_{entity} ({key}) names its rows, but it is not a column of \
a base table, so writes cannot be mapped to them: DISTINCT ON a column"
),
}
}
pub fn view_identity(entity: &str, view_oid: pgrx::pg_sys::Oid) -> crate::TViewResult<Identity> {
let key_column = format!("pk_{entity}");
let graph = walk::analyze(
view_oid,
&walk::Context {
tview_tables: &std::collections::HashMap::new(),
tview_views: &std::collections::HashMap::new(),
entity,
key_column: &key_column,
},
)?;
identity_of(entity, view_oid, &graph)
}
pub(super) fn identity_of(
entity: &str,
view_oid: pgrx::pg_sys::Oid,
graph: &QueryGraph,
) -> crate::TViewResult<Identity> {
use pgrx::prelude::*;
match graph
.identity
.clone()
.unwrap_or(Err(IdentityError::Missing))
{
Ok(walked) => Ok(Identity {
name: walked.name,
type_oid: walked.type_oid,
kind: walked.kind,
columns: walked
.columns
.iter()
.map(|c| (graph.occurrences[c.occ].relid, c.attnum))
.collect(),
}),
Err(error) => {
let viewdef = Spi::get_one_with_args::<String>(
"SELECT pg_catalog.pg_get_viewdef($1)",
&[crate::utils::spi::oid(view_oid)],
)
.map_err(|e| crate::TViewError::CatalogError {
operation: format!("Read the definition of the view of tv_{entity}"),
pg_error: e.to_string(),
})?
.unwrap_or_default();
Err(crate::TViewError::DefinitionRefused {
reason: identity_refusal(entity, error, &distinct_on_list(&viewdef)),
})
}
}
}