Skip to main content

uqa_sql/schema/sequences/
lifecycle.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Sequence rename and schema-move declaration rules.
8use crate::{
9    ast::{RelationPersistence, SequenceBound, SequenceLifecycle, SequenceOwnership},
10    SQLError,
11};
12use uqa_core::RelationIdentity;
13/// Namespace and ownership metadata needed to bind one sequence lifecycle target.
14pub trait SequenceLifecycleCatalog {
15    fn temporary_schema_name(&self) -> String;
16    fn sequence_is_owned(&self, relation: &RelationIdentity) -> bool;
17    fn relation_kind_at(&self, name: &str) -> Result<Option<&'static str>, String>;
18}
19pub fn validate_sequence_lifecycle_shape(
20    alter: &crate::ast::AlterSequence,
21) -> Result<(), SQLError> {
22    if alter.restart != crate::ast::SequenceRestart::Unchanged
23        || alter.increment.is_some()
24        || alter.start.is_some()
25        || alter.data_type.is_some()
26        || alter.min_value != SequenceBound::Unchanged
27        || alter.max_value != SequenceBound::Unchanged
28        || alter.cycle.is_some()
29        || alter.cache_size.is_some()
30        || alter.ownership != SequenceOwnership::Unchanged
31        || alter.persistence.is_some()
32        || alter.role_owner.is_some()
33    {
34        return Err(SQLError::Internal(
35            "ALTER SEQUENCE name lifecycle cannot contain definition changes".into(),
36        ));
37    }
38    Ok(())
39}
40
41/// Bind the declared destination before acquiring its namespace lock. Collision and namespace-kind checks require the refreshed catalog after that acquisition.
42pub fn sequence_lifecycle_target(
43    catalog: &dyn SequenceLifecycleCatalog,
44    source: &RelationIdentity,
45    lifecycle: &SequenceLifecycle,
46) -> Result<RelationIdentity, SQLError> {
47    match lifecycle {
48        SequenceLifecycle::Unchanged => Err(SQLError::Internal(
49            "sequence lifecycle executor received no action".into(),
50        )),
51        SequenceLifecycle::RenameTo { name } => {
52            let (schema, target_name) = RelationIdentity::parse_reference(name)
53                .map_err(|error| SQLError::Internal(format!("invalid sequence name: {error}")))?;
54            if schema.is_some() {
55                return Err(SQLError::Internal(
56                    "ALTER SEQUENCE RENAME TO produced a qualified target".into(),
57                ));
58            }
59            Ok(RelationIdentity::new(&source.schema, target_name))
60        }
61        SequenceLifecycle::SetSchema { schema } => {
62            let (qualifier, target_schema) = RelationIdentity::parse_reference(schema)
63                .map_err(|error| SQLError::Internal(format!("invalid schema name: {error}")))?;
64            if qualifier.is_some() {
65                return Err(SQLError::Internal(
66                    "ALTER SEQUENCE SET SCHEMA produced a qualified schema".into(),
67                ));
68            }
69            if catalog.sequence_is_owned(source) {
70                return Err(SQLError::Routine {
71                    sqlstate: "0A000".into(),
72                    message: "cannot move an owned sequence into another schema".into(),
73                });
74            }
75            Ok(RelationIdentity::new(target_schema, &source.name))
76        }
77    }
78}
79
80/// Validate the locked destination and report whether its name needs publication. Even an unchanged schema must first retain its namespace dependency and pass namespace-kind validation.
81pub fn validate_sequence_lifecycle_target(
82    catalog: &dyn SequenceLifecycleCatalog,
83    source: &RelationIdentity,
84    target: &RelationIdentity,
85    persistence: RelationPersistence,
86    lifecycle: &SequenceLifecycle,
87) -> Result<bool, SQLError> {
88    let rename = matches!(lifecycle, SequenceLifecycle::RenameTo { .. });
89    if !rename {
90        if persistence == RelationPersistence::Temporary
91            || target.schema == catalog.temporary_schema_name()
92        {
93            return Err(SQLError::Routine {
94                sqlstate: "0A000".into(),
95                message: "cannot move objects into or out of temporary schemas".into(),
96            });
97        }
98        if source.schema == "pg_toast" || target.schema == "pg_toast" {
99            return Err(SQLError::Routine {
100                sqlstate: "0A000".into(),
101                message: "cannot move objects into or out of TOAST schema".into(),
102            });
103        }
104        if target == source {
105            return Ok(false);
106        }
107    }
108    reject_sequence_lifecycle_collision(catalog, source, target, rename)?;
109    Ok(true)
110}
111
112fn reject_sequence_lifecycle_collision(
113    catalog: &dyn SequenceLifecycleCatalog,
114    source: &RelationIdentity,
115    target: &RelationIdentity,
116    rename: bool,
117) -> Result<(), SQLError> {
118    if target == source
119        || catalog
120            .relation_kind_at(&target.qualified_name())
121            .map_err(|error| {
122                SQLError::Internal(format!(
123                    "check sequence lifecycle target `{}`: {error}",
124                    target.qualified_name()
125                ))
126            })?
127            .is_some()
128    {
129        return Err(SQLError::Routine {
130            sqlstate: "42P07".into(),
131            message: if rename {
132                format!("relation \"{}\" already exists", target.name)
133            } else {
134                format!(
135                    "relation \"{}\" already exists in schema \"{}\"",
136                    target.name, target.schema
137                )
138            },
139        });
140    }
141    Ok(())
142}
143
144use crate::catalog::resolution::RelationResolution;
145
146/// Resolve absence before execution checks the actual relation's owner, namespace and requested kind.
147pub fn sequence_alter_relation(
148    resolution: crate::catalog::resolution::RelationResolution,
149    alter: &crate::ast::AlterSequence,
150) -> Result<Option<(String, &'static str)>, SQLError> {
151    match resolution {
152        RelationResolution::Found(name, kind) => Ok(Some((name, kind))),
153        RelationResolution::MissingRelation | RelationResolution::MissingSchema(_)
154            if alter.if_exists =>
155        {
156            Ok(None)
157        }
158        RelationResolution::MissingSchema(schema) => Err(SQLError::Routine {
159            sqlstate: "3F000".into(),
160            message: format!("schema \"{schema}\" does not exist"),
161        }),
162        RelationResolution::MissingRelation => Err(SQLError::Routine {
163            sqlstate: "42P01".into(),
164            message: format!("relation \"{}\" does not exist", alter.name),
165        }),
166    }
167}
168
169pub fn validate_sequence_alter_kind(
170    alter: &crate::ast::AlterSequence,
171    kind: &str,
172    local_name: &str,
173) -> Result<(), SQLError> {
174    if kind == "sequence" {
175        return Ok(());
176    }
177    let definition = alter.lifecycle == SequenceLifecycle::Unchanged
178        && alter.role_owner.is_none()
179        && alter.persistence.is_none();
180    Err(SQLError::Routine {
181        sqlstate: "42809".into(),
182        message: if definition {
183            format!("cannot open relation \"{local_name}\"")
184        } else {
185            format!("\"{local_name}\" is not a sequence")
186        },
187    })
188}
189
190#[cfg(test)]
191mod tests;