mod derive;
mod indexes;
mod register;
mod relations;
mod select;
use derive::derive;
pub(crate) use indexes::{ManagedIndex, index_name, managed_indexes};
use register::Registration;
pub(crate) use relations::view_source_columns;
use relations::{
create_backing_view, create_materialized_table, key_table_on_identity, populate_initial_data,
relation_exists, relation_oid, tview_exists,
};
#[cfg(test)]
mod tests;
thread_local! {
static WITHOUT_ROWS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(crate) fn without_rows<T>(create: impl FnOnce() -> T) -> T {
WITHOUT_ROWS.with(|w| w.set(true));
let result = create();
WITHOUT_ROWS.with(|w| w.set(false));
result
}
pub use select::ViewColumns;
pub(crate) use select::check_one_select;
use super::uncascaded::Declarations;
use crate::error::{TViewError, TViewResult};
use crate::utils::log_debug;
use pgrx::prelude::*;
pub(crate) fn current_schema() -> TViewResult<String> {
crate::utils::spi_get_string("SELECT current_schema()::text")
.map_err(|e| TViewError::CatalogError {
operation: "Get current schema".to_string(),
pg_error: e.to_string(),
})?
.ok_or_else(|| TViewError::CatalogError {
operation: "Get current schema".to_string(),
pg_error: "current_schema() returned NULL (no schema in search_path?)".to_string(),
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Storage {
pub logged: bool,
pub fillfactor: i32,
pub data_gin_index: bool,
}
impl Storage {
#[must_use]
pub fn from_settings() -> Self {
Self {
logged: !crate::config::unlogged_by_default(),
fillfactor: crate::config::fillfactor(),
data_gin_index: crate::config::data_gin_index(),
}
}
}
pub(crate) fn create_tview_in(
tview_name: &str,
select_sql: &str,
schema_name: &str,
group_keys: Option<&super::aggregate::GroupKeys>,
storage: Storage,
declarations: Option<Declarations>,
) -> TViewResult<u64> {
create_tview_inner(
tview_name,
select_sql,
schema_name,
group_keys,
storage,
declarations.unwrap_or_else(Declarations::from_settings),
)
}
pub(crate) fn normalize_definition(
entity_name: &str,
select_sql: &str,
) -> TViewResult<(String, ViewColumns)> {
select::normalize(entity_name, select_sql)
}
fn checked_definition(
tview_name: &str,
select_sql: &str,
aggregate: bool,
) -> TViewResult<(String, String, ViewColumns)> {
if tview_exists(tview_name)? {
return Err(TViewError::RelationExists {
name: tview_name.to_string(),
});
}
let named = tview_name.strip_prefix("tv_").unwrap_or(tview_name);
let (final_select_sql, final_schema) = normalize_definition(named, select_sql)?;
let entity = final_schema
.entity
.clone()
.ok_or_else(|| TViewError::RequiredColumnMissing {
column_name: format!("pk_{named}"),
context: format!(
"the definition of tv_{named}: a TVIEW's definition outputs a pk_<entity> \
column, the first of which names the entity"
),
})?;
crate::validation::validate_sql_identifier(&entity, "entity_name")?;
if aggregate {
select::check_aggregate(&final_select_sql, &entity)?;
}
Ok((entity, final_select_sql, final_schema))
}
fn create_backing_view_in(
view_schema: &str,
view_name: &str,
tview: &str,
definition: &str,
) -> TViewResult<pg_sys::Oid> {
if relation_exists(view_schema, view_name)?
&& !super::drop::reclaim_leftover_view(view_schema, view_name)?
{
return Err(TViewError::DefinitionRefused {
reason: format!(
"the backing view of {tview}, {view_schema}.{view_name}, is already taken by \
another relation"
),
});
}
super::in_extension_schema(|| create_backing_view(view_name, definition, view_schema))?;
relation_oid(view_schema, view_name)
}
fn create_tview_inner(
tview_name: &str,
select_sql: &str,
schema_name: &str,
group_keys: Option<&super::aggregate::GroupKeys>,
storage: Storage,
declarations: Declarations,
) -> TViewResult<u64> {
crate::revision::check();
log_debug!(
"create_tview start for '{}' in schema '{}'",
tview_name,
schema_name
);
super::lock_entity(tview_name.strip_prefix("tv_").unwrap_or(tview_name))?;
let (entity_name, final_select_sql, final_schema) =
checked_definition(tview_name, select_sql, group_keys.is_some())?;
let entity_name = entity_name.as_str();
let tv_table_name = format!("tv_{entity_name}");
let schema_name = schema_name.to_string();
let (view_schema, view_name) = super::backing_view_name(&schema_name, &tv_table_name);
let view_oid = create_backing_view_in(
&view_schema,
&view_name,
&format!("{schema_name}.{tv_table_name}"),
&final_select_sql,
)?;
let base_table_oids = crate::dependency::find_base_tables(&view_name, Some(&view_schema))?;
let derivation = derive(
entity_name,
group_keys,
&base_table_oids,
view_oid,
&declarations,
)?;
let lineage = &derivation.lineage;
let indexes = create_materialized_table(
&tv_table_name,
&final_schema,
&schema_name,
&lineage.identity.name,
storage,
view_oid,
)?;
let rows = if WITHOUT_ROWS.with(std::cell::Cell::get) {
0
} else {
populate_initial_data(&tv_table_name, &schema_name, view_oid)?
};
if group_keys.is_none() && lineage.tables.is_empty() {
return Err(TViewError::DefinitionRefused {
reason: format!(
"TVIEW '{tv_table_name}' can never be refreshed: its definition reads no table"
),
});
}
Registration {
entity: entity_name,
schema: &schema_name,
view_oid,
definition: &final_select_sql,
columns: &final_schema,
group_keys,
derivation: &derivation,
}
.write(declarations, false)?;
let table_oid = relation_oid(&schema_name, &tv_table_name)?;
crate::catalog::indexes::record(table_oid, &indexes)?;
super::privileges::follow(Some(table_oid), false)?;
crate::lifecycle::validity::mark(table_oid)?;
crate::dependency::install_triggers(
&crate::dependency::trigger_plan(&derivation.base_tables, lineage),
entity_name,
)?;
crate::cache::invalidate_all();
crate::audit::log_create(entity_name, &final_select_sql);
crate::audit::flush_audit_buffer()?;
Ok(rows)
}
pub fn reregister_metadata(
entity_name: &str,
schema_name: &str,
definition: &str,
) -> TViewResult<crate::dependency::TriggerPlan> {
let meta = crate::catalog::TviewMeta::load_to_rederive(entity_name)
.map_err(|e| TViewError::CatalogError {
operation: format!("Read the metadata of tv_{entity_name}"),
pg_error: e.to_string(),
})?
.ok_or_else(|| TViewError::MetadataNotFound {
entity: entity_name.to_string(),
})?;
let view_oid = meta.view_oid;
select::check_one_select(definition)?;
let schema = ViewColumns::classify(crate::utils::column_types(view_oid)?);
let (view_schema, view_name) = super::relation_name(view_oid)?;
let base_table_oids = crate::dependency::find_base_tables(&view_name, Some(&view_schema))?;
let group_keys = stored_group_keys(entity_name)?;
let declarations = Declarations::of(&meta);
let derivation = derive(
entity_name,
group_keys.as_ref(),
&base_table_oids,
view_oid,
&declarations,
)?;
key_table_on_identity(
schema_name,
&format!("tv_{entity_name}"),
&derivation.lineage.identity.name,
)?;
Registration {
entity: entity_name,
schema: schema_name,
view_oid,
definition,
columns: &schema,
group_keys: group_keys.as_ref(),
derivation: &derivation,
}
.write(declarations, true)?;
crate::cache::invalidate_all();
Ok(crate::dependency::trigger_plan(
&derivation.base_tables,
&derivation.lineage,
))
}
pub fn reregister_tview(entity: &str) -> TViewResult<()> {
let meta = crate::catalog::TviewMeta::load_to_rederive(entity)?.ok_or_else(|| {
TViewError::MetadataNotFound {
entity: entity.to_string(),
}
})?;
crate::owner::require_owner(meta.tview_oid, &format!("tv_{entity}"))?;
super::lock_entity(entity)?;
let (definition, schema_name) = Spi::connect(|client| {
let args = [crate::utils::spi::text(entity)];
client
.select(
&format!(
"SELECT m.definition, n.nspname::text \
FROM {} m \
JOIN pg_catalog.pg_class c ON c.oid = m.table_oid \
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \
WHERE m.entity = $1",
crate::utils::meta_table()
),
None,
&args,
)?
.first()
.get_two::<String, String>()
})
.map_err(|e| TViewError::CatalogError {
operation: format!("Read the definition of TVIEW {entity}"),
pg_error: e.to_string(),
})?;
let (Some(definition), Some(schema_name)) = (definition, schema_name) else {
return Err(TViewError::MetadataNotFound {
entity: entity.to_string(),
});
};
let plan = reregister_metadata(entity, &schema_name, &definition)?;
crate::dependency::sync_entity_triggers(&plan, entity)?;
let _owner = crate::owner::AsOwner::of_extension()?;
Spi::run_with_args(
&format!(
"UPDATE {} SET needs_reregister = false WHERE entity = $1",
crate::utils::meta_table()
),
&[crate::utils::spi::text(entity)],
)
.map_err(|e| TViewError::CatalogError {
operation: format!("Clear needs_reregister of TVIEW {entity}"),
pg_error: e.to_string(),
})
}
pub(crate) fn stored_group_keys(
entity_name: &str,
) -> TViewResult<Option<super::aggregate::GroupKeys>> {
let stored: Option<pgrx::JsonB> = Spi::get_one_with_args(
&format!(
"SELECT group_keys FROM {} WHERE entity = $1",
crate::utils::meta_table()
),
&[crate::utils::spi::text(entity_name)],
)
.map_err(|e| TViewError::CatalogError {
operation: "Read group_keys".to_string(),
pg_error: e.to_string(),
})?;
stored
.map(|j| {
serde_json::from_value(j.0).map_err(|e| TViewError::CatalogError {
operation: format!("Read the group keys of tv_{entity_name}"),
pg_error: e.to_string(),
})
})
.transpose()
}