mod derive;
mod indexes;
mod json_indexes;
mod register;
mod relations;
mod select;
mod storage;
use derive::derive;
pub(crate) use indexes::{ManagedIndex, index_name, key_index, managed_indexes};
pub use json_indexes::JsonIndex;
pub(crate) use json_indexes::{
apply as apply_json_indexes, declared as declared_json_indexes, present as present_json_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,
};
pub use storage::{Compression, DataStorage, Storage, TOAST_TUPLE_TARGET};
#[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(),
})
}
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::defaults),
)
}
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 {
fill_new(&schema_name, &tv_table_name, view_oid, lineage)?
};
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)?;
finish(
entity_name,
table_oid,
&indexes,
&derivation,
&final_select_sql,
)?;
Ok(rows)
}
fn fill_new(
schema: &str,
table: &str,
view_oid: pg_sys::Oid,
lineage: &crate::lineage::Lineage,
) -> TViewResult<u64> {
if lineage.set_operation {
crate::refresh::refuse_duplicate_keys_in(
&format!(
"{}.{}",
crate::utils::ident::quoted(schema),
crate::utils::ident::quoted(table)
),
&lineage.identity.name,
&crate::utils::qualified_relname_from_oid(view_oid)?,
"true",
&[],
)?;
}
populate_initial_data(table, schema, view_oid)
}
fn finish(
entity: &str,
table_oid: pg_sys::Oid,
indexes: &[String],
derivation: &derive::Derivation,
definition: &str,
) -> TViewResult<()> {
crate::catalog::indexes::record(table_oid, indexes)?;
super::privileges::follow(Some(table_oid), false)?;
crate::lifecycle::validity::mark(entity, table_oid)?;
crate::dependency::install_triggers(
&crate::dependency::trigger_plan(&derivation.base_tables, &derivation.lineage),
entity,
)?;
crate::cache::invalidate_all();
crate::audit::log_create(entity, definition);
crate::audit::flush_audit_buffer()
}
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::TviewNotFound {
name: 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::TviewNotFound {
name: entity.to_string(),
}
})?;
crate::owner::require_owner(meta.tview_oid, &format!("tv_{entity}"))?;
super::lock_entity(entity)?;
let (definition, schema_name) = crate::catalog::row::definition_and_schema(entity)?;
let (Some(definition), Some(schema_name)) = (definition, schema_name) else {
return Err(TViewError::TviewNotFound {
name: entity.to_string(),
});
};
let plan = reregister_metadata(entity, &schema_name, &definition)?;
crate::dependency::sync_entity_triggers(&plan, entity)?;
crate::catalog::row::clear_needs_reregister(entity)
}
pub(crate) fn stored_group_keys(
entity_name: &str,
) -> TViewResult<Option<super::aggregate::GroupKeys>> {
crate::catalog::row::group_keys(entity_name)?
.map(|keys| {
serde_json::from_value(keys).map_err(|e| TViewError::CatalogError {
operation: format!("Read the group keys of tv_{entity_name}"),
pg_error: e.to_string(),
})
})
.transpose()
}