use rupg_catalog::{RelKind, Relation};
use rupg_common::{Error, Result, SqlState};
use rupg_pgcatalog::builtin::{self, Named};
use rupg_sql::nodes::{Node, TypeName};
use super::super::alter_column::attribute_storage;
use super::super::partition_attach::without_locations;
use super::super::{Definer, Found, Message, not_yet, relkind_detail};
use super::{NewColumn, Plan, RawDefault};
use crate::typename::place;
use crate::types;
fn storage_name(storage: u8) -> &'static str {
match storage {
b'p' => "PLAIN",
b'm' => "MAIN",
b'x' => "EXTENDED",
b'e' => "EXTERNAL",
_ => "???",
}
}
pub(super) fn storage_of(ty: u32, storage: u8) -> u8 {
if storage == 0 { types::row(ty).map_or(b'x', |t| t.storage) } else { storage }
}
fn compression_name(compression: u8) -> &'static str {
match compression {
b'p' => "pglz",
b'l' => "lz4",
_ => "",
}
}
pub(in crate::ddl) fn collation_name(collation: u32) -> &'static str {
builtin::named_by_oid(Named::Collation, collation).map_or("???", |n| n.name)
}
pub(super) fn merge_generated(generated: u8, column: &NewColumn) -> Result<()> {
let name = &column.name;
if generated != 0 {
if column.default.is_some() && column.generated == 0 {
return Err(Error::new(
SqlState::INVALID_COLUMN_DEFINITION,
format!("column \"{name}\" inherits from generated column but specifies default"),
));
}
if column.identity != 0 {
return Err(Error::new(
SqlState::INVALID_COLUMN_DEFINITION,
format!("column \"{name}\" inherits from generated column but specifies identity"),
));
}
} else if column.generated != 0 {
return Err(Error::new(
SqlState::INVALID_COLUMN_DEFINITION,
format!("child column \"{name}\" specifies generation expression"),
)
.with_hint("A child table column cannot be generated unless its parent column is."));
}
if generated != 0 && column.generated != 0 && generated != column.generated {
let kind = |g: u8| if g == b's' { "STORED" } else { "VIRTUAL" };
return Err(Error::new(
SqlState::INVALID_COLUMN_DEFINITION,
format!("column \"{name}\" inherits from generated column of different kind"),
)
.with_detail(format!(
"Parent column is {}, child column is {}.",
kind(generated),
kind(column.generated)
)));
}
Ok(())
}
fn conflict(state: SqlState, message: String, first: &str, second: &str) -> Error {
Error::new(state, message).with_detail(format!("{first} versus {second}"))
}
impl Definer<'_, '_> {
pub(super) fn inherit_parents(&self, inh: &[Option<Node>]) -> Result<Vec<u32>> {
let mut parents = Vec::new();
for node in inh.iter().flatten() {
let Node::RangeVar(rv) = node else { continue };
let Found::User(oid) = self.lookup_relation(rv)? else {
return Err(not_yet("INHERITS of a system catalog", place(rv.location)));
};
if parents.contains(&oid) {
let name = self.catalog.relation(oid).map(|r| r.name.clone()).unwrap_or_default();
return Err(Error::new(
SqlState::DUPLICATE_TABLE,
format!("relation \"{name}\" would be inherited from more than once"),
));
}
parents.push(oid);
}
Ok(parents)
}
fn inherit_parent(&self, parent: u32, namespace: u32) -> Result<Relation> {
let rel =
self.catalog.relation(parent).cloned().ok_or_else(|| {
Error::internal(format!("cache lookup failed for relation {parent}"))
})?;
let name = &rel.name;
let kind = self.kind_of(Found::User(parent));
if matches!(kind, b'i' | b'I' | b'c') {
return Err(Error::new(
SqlState::WRONG_OBJECT_TYPE,
format!("cannot open relation \"{name}\""),
)
.with_detail(relkind_detail(kind)));
}
let wrong = |message: String| Error::new(SqlState::WRONG_OBJECT_TYPE, message);
if rel.kind == RelKind::Partitioned {
return Err(wrong(format!("cannot inherit from partitioned table \"{name}\"")));
}
if rel.partition_of.is_some() {
return Err(wrong(format!("cannot inherit from partition \"{name}\"")));
}
if !matches!(kind, b'r' | b'f') {
return Err(wrong(format!(
"inherited relation \"{name}\" is not a table or foreign table"
)));
}
let temp = self.catalog.is_temp_schema(rel.namespace);
if temp && !self.catalog.is_temp_schema(namespace) {
return Err(wrong(format!("cannot inherit from temporary relation \"{name}\"")));
}
if temp && rel.namespace != self.an.temp {
return Err(wrong("cannot inherit from temporary relation of another session".into()));
}
if !self.is_superuser() && rel.owner != self.user {
return Err(Error::new(
SqlState::INSUFFICIENT_PRIVILEGE,
format!("must be owner of table {name}"),
));
}
Ok(rel)
}
pub(super) fn merge_inherited_columns(
&mut self,
plan: &mut Plan,
parents: &[u32],
namespace: u32,
) -> Result<()> {
let local = std::mem::take(&mut plan.columns);
for (i, column) in local.iter().enumerate() {
if local[..i].iter().any(|c| c.name == column.name) {
return Err(Error::new(
SqlState::DUPLICATE_COLUMN,
format!("column \"{}\" specified more than once", column.name),
));
}
}
let mut columns: Vec<NewColumn> = Vec::new();
let mut bogus: Vec<String> = Vec::new();
for &parent in parents {
let rel = self.inherit_parent(parent, namespace)?;
for (i, c) in rel.columns.iter().enumerate().filter(|(_, c)| !c.dropped) {
let attnum = i16::try_from(i + 1).unwrap_or(i16::MAX);
let default = c
.has_default
.then(|| self.catalog.default_of(parent, attnum))
.flatten()
.map(|d| d.expr.clone());
let Some(prev) = columns.iter_mut().find(|n| n.name == c.name) else {
let mut inherited = c.clone();
inherited.identity = 0;
inherited.local = false;
inherited.inhcount = 1;
columns.push(NewColumn {
name: c.name.clone(),
type_name: TypeName::default(),
collate: None,
collation: c.collation,
is_not_null: false,
default: default.map(|text| RawDefault::Cooked(text, parent)),
identity: 0,
generated: c.generated,
storage: None,
compression: None,
inherited: Some(inherited),
});
continue;
};
self.notice(
SqlState::SUCCESSFUL_COMPLETION,
format!("merging multiple inherited definitions of column \"{}\"", c.name),
);
let Some(inh) = prev.inherited.as_mut() else { continue };
if (inh.ty, inh.typmod) != (c.ty, c.typmod) {
return Err(conflict(
SqlState::DATATYPE_MISMATCH,
format!("inherited column \"{}\" has a type conflict", c.name),
&types::with_typmod(inh.ty, inh.typmod),
&types::with_typmod(c.ty, c.typmod),
));
}
if inh.collation != c.collation {
return Err(conflict(
SqlState::COLLATION_MISMATCH,
format!("inherited column \"{}\" has a collation conflict", c.name),
&format!("\"{}\"", collation_name(inh.collation)),
&format!("\"{}\"", collation_name(c.collation)),
));
}
let (mine, theirs) = (storage_of(inh.ty, inh.storage), storage_of(c.ty, c.storage));
if mine != theirs {
return Err(conflict(
SqlState::DATATYPE_MISMATCH,
format!("inherited column \"{}\" has a storage parameter conflict", c.name),
storage_name(mine),
storage_name(theirs),
));
}
if inh.compression == 0 {
inh.compression = c.compression;
} else if c.compression != 0 && inh.compression != c.compression {
return Err(conflict(
SqlState::DATATYPE_MISMATCH,
format!("column \"{}\" has a compression method conflict", c.name),
compression_name(inh.compression),
compression_name(c.compression),
));
}
if inh.generated != c.generated {
return Err(Error::new(
SqlState::DATATYPE_MISMATCH,
format!("inherited column \"{}\" has a generation conflict", c.name),
));
}
inh.inhcount = inh.inhcount.saturating_add(1);
match (&prev.default, default) {
(None, Some(text)) => prev.default = Some(RawDefault::Cooked(text, parent)),
(Some(RawDefault::Cooked(old, _)), Some(text))
if without_locations(Some(old)) != without_locations(Some(&text))
&& !bogus.contains(&c.name) =>
{
bogus.push(c.name.clone());
}
_ => {}
}
}
}
let mut position = 0;
let mut places = Vec::with_capacity(local.len());
for column in local {
position += 1;
let Some(at) = columns.iter().position(|c| c.name == column.name) else {
places.push(columns.len());
columns.push(column);
continue;
};
let name = column.name.clone();
let message = if at + 1 == position {
Error::new(
SqlState::SUCCESSFUL_COMPLETION,
format!("merging column \"{name}\" with inherited definition"),
)
} else {
Error::new(
SqlState::SUCCESSFUL_COMPLETION,
format!("moving and merging column \"{name}\" with inherited definition"),
)
.with_detail("User-specified column moved to the position of the inherited column.")
};
self.flush();
self.messages.push(Message::Notice(message));
let new = self.catalog_column(&column)?;
let target = &mut columns[at];
let Some(inh) = target.inherited.as_mut() else { continue };
if (inh.ty, inh.typmod) != (new.ty, new.typmod) {
return Err(conflict(
SqlState::DATATYPE_MISMATCH,
format!("column \"{name}\" has a type conflict"),
&types::with_typmod(inh.ty, inh.typmod),
&types::with_typmod(new.ty, new.typmod),
));
}
if inh.collation != new.collation {
return Err(conflict(
SqlState::COLLATION_MISMATCH,
format!("column \"{name}\" has a collation conflict"),
&format!("\"{}\"", collation_name(inh.collation)),
&format!("\"{}\"", collation_name(new.collation)),
));
}
inh.identity = new.identity;
if let Some(storage) = &column.storage {
let storage = attribute_storage(new.ty, storage)?;
if storage_of(inh.ty, inh.storage) != storage {
return Err(conflict(
SqlState::DATATYPE_MISMATCH,
format!("column \"{name}\" has a storage parameter conflict"),
storage_name(storage_of(inh.ty, inh.storage)),
storage_name(storage),
));
}
}
if column.compression.is_some() {
if inh.compression == 0 {
inh.compression = new.compression;
} else if inh.compression != new.compression {
return Err(conflict(
SqlState::DATATYPE_MISMATCH,
format!("column \"{name}\" has a compression method conflict"),
compression_name(inh.compression),
compression_name(new.compression),
));
}
}
merge_generated(inh.generated, &column)?;
inh.local = true;
target.is_not_null |= column.is_not_null;
if column.default.is_some() {
target.default = column.default;
bogus.retain(|b| *b != name);
}
places.push(at);
}
if let Some(c) = columns.iter().find(|c| bogus.contains(&c.name)) {
let name = &c.name;
return Err(if c.generated != 0 {
Error::new(
SqlState::INVALID_COLUMN_DEFINITION,
format!("column \"{name}\" inherits conflicting generation expressions"),
)
.with_hint("To resolve the conflict, specify a generation expression explicitly.")
} else {
Error::new(
SqlState::INVALID_COLUMN_DEFINITION,
format!("column \"{name}\" inherits conflicting default values"),
)
.with_hint("To resolve the conflict, specify a default explicitly.")
});
}
for serial in &mut plan.serials {
serial.column = places.get(serial.column).copied().unwrap_or(serial.column);
}
plan.columns = columns;
Ok(())
}
}