use crate::ast::identifiers::ObjectId;
use crate::model::column::Column;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Privilege {
Select,
Insert,
Update,
Delete,
Truncate,
References,
Trigger,
All,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct PrivilegeMatrix {
pub grants: HashMap<ObjectId, HashSet<Privilege>>,
}
impl PrivilegeMatrix {
pub fn grant(&mut self, role: ObjectId, privileges: HashSet<Privilege>) {
self.grants.entry(role).or_default().extend(privileges);
}
pub fn revoke(&mut self, role: &ObjectId, privileges: &HashSet<Privilege>) {
if let Some(owned) = self.grants.get_mut(role) {
if privileges.contains(&Privilege::All) {
owned.clear();
} else {
for p in privileges {
owned.remove(p);
}
}
}
}
pub fn has_privilege(&self, role: &ObjectId, privilege: Privilege) -> bool {
self.grants.get(role).is_some_and(|set| {
set.contains(&privilege)
|| (privilege != Privilege::All && set.contains(&Privilege::All))
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RelationKind {
Table,
View,
MaterializedView,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Persistence {
Permanent,
Temporary,
Unlogged,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RelationState {
pub id: ObjectId,
pub owner: ObjectId,
pub columns: Vec<Column>,
pub generation: u64,
pub estimated_rows: Option<u64>,
pub relpages: Option<u64>,
pub kind: RelationKind,
pub persistence: Persistence,
pub triggers: HashSet<String>,
pub policies: HashSet<String>,
pub last_analyze: Option<String>,
pub last_autoanalyze: Option<String>,
pub created_at_tx_depth: usize, pub privileges: PrivilegeMatrix,
pub partition_type: Option<String>, pub partition_by: Option<String>, #[serde(default)]
pub is_fk_dependency: bool,
}
impl Default for RelationState {
fn default() -> Self {
Self {
id: ObjectId::new("public", "dummy"),
owner: ObjectId::new("public", "postgres"),
columns: Vec::new(),
generation: 0,
estimated_rows: Some(0),
relpages: None,
kind: RelationKind::Table,
persistence: Persistence::Permanent,
triggers: HashSet::new(),
policies: HashSet::new(),
last_analyze: None,
last_autoanalyze: None,
created_at_tx_depth: 0,
privileges: PrivilegeMatrix::default(),
partition_type: None,
partition_by: None,
is_fk_dependency: false,
}
}
}
impl RelationState {
pub fn new(
id: ObjectId,
owner: ObjectId,
generation: u64,
estimated_rows: Option<u64>,
kind: RelationKind,
persistence: Persistence,
created_at_tx_depth: usize,
) -> Self {
Self {
id,
owner,
columns: Vec::new(),
generation,
estimated_rows,
relpages: None,
kind,
persistence,
triggers: HashSet::new(),
policies: HashSet::new(),
last_analyze: None,
last_autoanalyze: None,
created_at_tx_depth,
privileges: PrivilegeMatrix::default(),
partition_type: None,
partition_by: None,
is_fk_dependency: false,
}
}
pub fn mark_fk_dependency(&mut self) {
self.is_fk_dependency = true;
}
pub fn apply_column_action(&mut self, action: &ColumnAction) {
match action {
ColumnAction::Add {
name,
data_type,
not_null,
default,
} => {
if !self.columns.iter().any(|c| c.name == *name) {
let serial_type = data_type
.as_deref()
.map(str::trim)
.map(str::to_ascii_lowercase)
.and_then(|ty| match ty.as_str() {
"smallserial" | "serial2" => Some("smallint"),
"serial" | "serial4" => Some("integer"),
"bigserial" | "serial8" => Some("bigint"),
_ => None,
});
let is_serial = serial_type.is_some();
let normalized_default = if is_serial {
Some(crate::analysis::expr_ir::ExprIr::FunctionCall {
name: "nextval".to_string(),
args: Vec::new(),
})
} else if matches!(
default,
Some(crate::analysis::expr_ir::ExprIr::Literal(value))
if value.trim().eq_ignore_ascii_case("null")
) {
None
} else {
default.clone()
};
self.columns.push(Column {
name: name.clone(),
data_type: serial_type
.map(str::to_string)
.or_else(|| data_type.clone()),
default: normalized_default,
is_nullable: !(*not_null || is_serial),
avg_width: None,
default_expr_text: None,
type_modifier: None,
});
}
}
ColumnAction::Drop { name } => {
self.columns.retain(|c| c.name != *name);
}
ColumnAction::Rename { from, to } => {
if let Some(pos) = self.columns.iter().position(|c| c.name == *from)
&& !self.columns.iter().any(|c| c.name == *to)
{
self.columns[pos].name = to.clone();
}
}
ColumnAction::SetNotNull { name } => {
if let Some(col) = self.columns.iter_mut().find(|c| c.name == *name) {
col.is_nullable = false;
}
}
ColumnAction::DropNotNull { name } => {
if let Some(col) = self.columns.iter_mut().find(|c| c.name == *name) {
col.is_nullable = true;
}
}
ColumnAction::SetType { name, data_type } => {
if let Some(col) = self.columns.iter_mut().find(|c| c.name == *name) {
col.data_type = Some(data_type.clone());
}
}
ColumnAction::SetDefault { name, default } => {
if let Some(col) = self.columns.iter_mut().find(|c| c.name == *name) {
col.default = if matches!(
default,
Some(crate::analysis::expr_ir::ExprIr::Literal(value))
if value.trim().eq_ignore_ascii_case("null")
) {
None
} else {
default.clone()
};
col.default_expr_text = None;
}
}
}
}
pub fn has_column(&self, name: &str) -> bool {
self.columns.iter().any(|c| c.name == name)
}
pub fn get_column(&self, name: &str) -> Option<&Column> {
self.columns.iter().find(|c| c.name == name)
}
pub fn is_stale(&self) -> bool {
self.last_analyze.is_none() && self.last_autoanalyze.is_none()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ColumnAction {
Add {
name: String,
data_type: Option<String>,
not_null: bool,
default: Option<crate::analysis::expr_ir::ExprIr>,
},
Drop {
name: String,
},
Rename {
from: String,
to: String,
},
SetNotNull {
name: String,
},
DropNotNull {
name: String,
},
SetType {
name: String,
data_type: String,
},
SetDefault {
name: String,
default: Option<crate::analysis::expr_ir::ExprIr>,
},
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq)]
pub enum RelationOverlay {
Present(RelationState),
Dropped,
}