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