Skip to main content

uqa_sql/catalog/
view.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7use crate::{RowSchema, SQLError};
8
9mod materialized;
10pub use materialized::materialized_view_schema;
11
12/// Scanning an unpopulated materialized view fails even if the query would read no rows.
13pub fn require_materialized_view_population(name: &str, populated: bool) -> Result<(), SQLError> {
14    if populated {
15        return Ok(());
16    }
17    let (_, relation) =
18        uqa_core::RelationIdentity::parse_reference(name).map_err(SQLError::Unsupported)?;
19    Err(SQLError::Diagnostic {
20        sqlstate: "55000".into(),
21        message: format!("materialized view \"{relation}\" has not been populated"),
22        detail: None,
23        hint: Some("Use the REFRESH MATERIALIZED VIEW command.".into()),
24    })
25}
26
27pub fn create_view_output_columns(
28    schema: &RowSchema,
29    declared: &[String],
30) -> Result<Vec<String>, SQLError> {
31    if declared.len() > schema.len() {
32        return Err(SQLError::Routine {
33            sqlstate: "42601".into(),
34            message: "CREATE VIEW specifies more column names than columns".into(),
35        });
36    }
37    let columns = schema
38        .columns()
39        .iter()
40        .enumerate()
41        .map(|(position, column)| {
42            declared
43                .get(position)
44                .cloned()
45                .unwrap_or_else(|| schema.public_name(position).unwrap_or(column).to_string())
46        })
47        .collect::<Vec<_>>();
48    let mut seen = std::collections::BTreeSet::new();
49    for column in &columns {
50        if !seen.insert(column) {
51            return Err(SQLError::Routine {
52                sqlstate: "42701".into(),
53                message: format!("column \"{column}\" specified more than once"),
54            });
55        }
56    }
57    Ok(columns)
58}
59
60pub fn named_view_schema(
61    query_schema: &RowSchema,
62    output_columns: &[String],
63) -> Result<RowSchema, SQLError> {
64    if query_schema.len() != output_columns.len() {
65        return Err(SQLError::Internal(format!(
66            "stored view row type has {} columns but its query has {}",
67            output_columns.len(),
68            query_schema.len()
69        )));
70    }
71    Ok(RowSchema::with_types(
72        output_columns.to_vec(),
73        query_schema.column_types().to_vec(),
74    )
75    .with_column_collations(
76        (0..query_schema.len()).map(|index| query_schema.column_collation(index)),
77    )
78    .with_record_fields_from(query_schema))
79}
80
81pub fn validate_view_column_types(
82    query_schema: &RowSchema,
83    output_columns: &[String],
84) -> Result<(), SQLError> {
85    for (position, column) in output_columns.iter().enumerate() {
86        if let Some(ty) = query_schema.column_type(position) {
87            crate::schema::columns::validate_postgres_relation_column_type(column, ty)?;
88        }
89    }
90    Ok(())
91}
92
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
94pub enum StoredViewKind {
95    #[default]
96    View,
97    Materialized,
98}
99
100#[derive(Debug, Clone, Copy, Default)]
101pub struct ViewMutationCapabilities {
102    pub insertable: bool,
103    pub updatable: bool,
104    pub deletable: bool,
105}
106impl ViewMutationCapabilities {
107    pub const fn fully_updatable(self) -> bool {
108        self.updatable && self.deletable
109    }
110}
111
112#[derive(Clone)]
113pub struct ViewRewriteDefinition {
114    pub query: crate::plan::QueryPlan,
115    pub output_columns: Option<Vec<String>>,
116    pub options: Vec<(String, String)>,
117    pub kind: StoredViewKind,
118    pub materialized_column_types: Vec<Option<crate::ColumnType>>,
119    pub materialized_column_collations: Option<Vec<u32>>,
120}
121
122impl ViewRewriteDefinition {
123    pub fn row_schema(
124        &self,
125        routines: &dyn crate::routines::RoutineResolution,
126        catalog: super::analysis::CatalogReadView,
127        resolution: super::resolution::RelationNameResolution,
128    ) -> Result<crate::RowSchema, crate::SQLError> {
129        use crate::catalog::view::{create_view_output_columns, named_view_schema};
130        if self.kind == StoredViewKind::Materialized {
131            return materialized_view_schema(
132                self.output_columns.as_deref().unwrap_or_default(),
133                &self.materialized_column_types,
134                self.materialized_column_collations.as_deref(),
135            );
136        }
137        let query_schema = crate::binding::analyze_query_plan_schema_with_catalog(
138            routines,
139            &self.query,
140            &[],
141            catalog,
142            resolution,
143        )?;
144        let output_columns = match &self.output_columns {
145            Some(columns) => columns.clone(),
146            None => create_view_output_columns(&query_schema, &[])?,
147        };
148        named_view_schema(&query_schema, &output_columns)
149    }
150}
151
152#[cfg(test)]
153mod tests;