Skip to main content

pgevolve_core/parse/builder/
create_seq_stmt.rs

1//! `CREATE SEQUENCE` → [`crate::ir::sequence::Sequence`].
2
3use pg_query::NodeEnum;
4use pg_query::protobuf::{AConst, CreateSeqStmt, DefElem, a_const};
5
6use crate::identifier::Identifier;
7use crate::ir::column_type::ColumnType;
8use crate::ir::sequence::{Sequence, SequenceOwner};
9use crate::parse::builder::shared;
10use crate::parse::error::{ParseError, SourceLocation};
11
12/// Build a [`Sequence`] from a `CREATE SEQUENCE` AST.
13pub fn build_sequence(
14    stmt: &CreateSeqStmt,
15    default_schema: Option<&Identifier>,
16    location: &SourceLocation,
17) -> Result<Sequence, ParseError> {
18    let range = stmt
19        .sequence
20        .as_ref()
21        .ok_or_else(|| ParseError::Structural {
22            location: location.clone(),
23            message: "CREATE SEQUENCE missing sequence name".into(),
24        })?;
25    let qname = shared::resolve_qname(range, default_schema, location)?;
26
27    // Postgres defaults — `bigint`, start=1, increment=1, cache=1, no cycle.
28    let mut data_type = ColumnType::BigInt;
29    let mut start: i64 = 1;
30    let mut increment: i64 = 1;
31    let mut min_value: Option<i64> = None;
32    let mut max_value: Option<i64> = None;
33    let mut cache: i64 = 1;
34    let mut cycle = false;
35    let mut owned_by: Option<SequenceOwner> = None;
36
37    for opt in &stmt.options {
38        let Some(NodeEnum::DefElem(de)) = opt.node.as_ref() else {
39            continue;
40        };
41        match de.defname.as_str() {
42            "as" => {
43                if let Some(t) = type_name_of(de)
44                    && let Ok(parsed) = ColumnType::parse_from_pg_type_string(&t)
45                {
46                    data_type = parsed;
47                }
48            }
49            "start" => {
50                if let Some(v) = i64_of(de) {
51                    start = v;
52                }
53            }
54            "increment" => {
55                if let Some(v) = i64_of(de) {
56                    increment = v;
57                }
58            }
59            "minvalue" => {
60                min_value = i64_of(de);
61            }
62            "maxvalue" => {
63                max_value = i64_of(de);
64            }
65            "cache" => {
66                if let Some(v) = i64_of(de) {
67                    cache = v;
68                }
69            }
70            "cycle" => {
71                cycle = bool_of(de).unwrap_or(true);
72            }
73            "owned_by" => {
74                owned_by = parse_owned_by(de, default_schema, location)?;
75            }
76            _ => {}
77        }
78    }
79
80    // If start is unset (option absent) and the type is signed, Postgres defaults
81    // to 1 — already the value above. Likewise for `min/max_value` we keep `None`
82    // meaning "use the type's min/max".
83
84    Ok(Sequence {
85        qname,
86        data_type,
87        start,
88        increment,
89        min_value,
90        max_value,
91        cache,
92        cycle,
93        owned_by,
94        comment: None,
95        owner: None,
96        grants: vec![],
97    })
98}
99
100fn i64_of(de: &DefElem) -> Option<i64> {
101    let arg = de.arg.as_ref()?.node.as_ref()?;
102    match arg {
103        NodeEnum::Integer(i) => Some(i64::from(i.ival)),
104        NodeEnum::Float(f) => f.fval.parse::<i64>().ok(),
105        NodeEnum::AConst(c) => aconst_int(c),
106        _ => None,
107    }
108}
109
110fn bool_of(de: &DefElem) -> Option<bool> {
111    let arg = de.arg.as_ref()?.node.as_ref()?;
112    match arg {
113        NodeEnum::Boolean(b) => Some(b.boolval),
114        NodeEnum::Integer(i) => Some(i.ival != 0),
115        NodeEnum::AConst(c) => match c.val.as_ref()? {
116            a_const::Val::Boolval(b) => Some(b.boolval),
117            a_const::Val::Ival(i) => Some(i.ival != 0),
118            _ => None,
119        },
120        _ => None,
121    }
122}
123
124fn aconst_int(c: &AConst) -> Option<i64> {
125    match c.val.as_ref()? {
126        a_const::Val::Ival(i) => Some(i64::from(i.ival)),
127        a_const::Val::Fval(f) => f.fval.parse::<i64>().ok(),
128        _ => None,
129    }
130}
131
132fn type_name_of(de: &DefElem) -> Option<String> {
133    let arg = de.arg.as_ref()?.node.as_ref()?;
134    if let NodeEnum::TypeName(tn) = arg {
135        return shared::render_type_name_to_string(tn);
136    }
137    None
138}
139
140/// `OWNED BY <table>.<column>` is encoded as a list of String nodes.
141fn parse_owned_by(
142    de: &DefElem,
143    default_schema: Option<&Identifier>,
144    location: &SourceLocation,
145) -> Result<Option<SequenceOwner>, ParseError> {
146    let Some(arg) = de.arg.as_ref().and_then(|a| a.node.as_ref()) else {
147        return Ok(None);
148    };
149    let NodeEnum::List(list) = arg else {
150        return Ok(None);
151    };
152    let parts: Vec<&str> = list
153        .items
154        .iter()
155        .filter_map(|n| match n.node.as_ref() {
156            Some(NodeEnum::String(s)) => Some(s.sval.as_str()),
157            _ => None,
158        })
159        .collect();
160    match parts.as_slice() {
161        // `OWNED BY NONE` is a single "none" string.
162        [first] if first.eq_ignore_ascii_case("none") => Ok(None),
163        [table, column] => {
164            let schema = default_schema
165                .cloned()
166                .ok_or_else(|| ParseError::UnqualifiedName {
167                    location: location.clone(),
168                })?;
169            Ok(Some(SequenceOwner {
170                table: crate::identifier::QualifiedName::new(
171                    schema,
172                    shared::ident(table, location)?,
173                ),
174                column: shared::ident(column, location)?,
175            }))
176        }
177        [schema, table, column] => Ok(Some(SequenceOwner {
178            table: crate::identifier::QualifiedName::new(
179                shared::ident(schema, location)?,
180                shared::ident(table, location)?,
181            ),
182            column: shared::ident(column, location)?,
183        })),
184        _ => Err(ParseError::Structural {
185            location: location.clone(),
186            message: "OWNED BY must reference table.column or schema.table.column".into(),
187        }),
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use std::path::PathBuf;
195
196    fn loc() -> SourceLocation {
197        SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
198    }
199
200    fn build(sql: &str) -> Sequence {
201        let parsed = pg_query::parse(sql).expect("parses");
202        let stmt = parsed
203            .protobuf
204            .stmts
205            .into_iter()
206            .next()
207            .and_then(|raw| raw.stmt)
208            .and_then(|n| n.node)
209            .expect("stmt");
210        let NodeEnum::CreateSeqStmt(s) = stmt else {
211            panic!()
212        };
213        build_sequence(&s, None, &loc()).expect("builds")
214    }
215
216    #[test]
217    fn defaults_for_bare_create() {
218        let s = build("CREATE SEQUENCE app.s1;");
219        assert_eq!(s.qname.to_string(), "app.s1");
220        assert_eq!(s.data_type, ColumnType::BigInt);
221        assert_eq!(s.start, 1);
222        assert_eq!(s.increment, 1);
223        assert_eq!(s.cache, 1);
224        assert!(!s.cycle);
225        assert!(s.owned_by.is_none());
226    }
227
228    #[test]
229    fn explicit_options_extracted() {
230        let s = build(
231            "CREATE SEQUENCE app.s1 AS integer INCREMENT BY 2 START WITH 10 MINVALUE 5 \
232             MAXVALUE 1000 CACHE 50 CYCLE;",
233        );
234        assert_eq!(s.data_type, ColumnType::Integer);
235        assert_eq!(s.start, 10);
236        assert_eq!(s.increment, 2);
237        assert_eq!(s.min_value, Some(5));
238        assert_eq!(s.max_value, Some(1000));
239        assert_eq!(s.cache, 50);
240        assert!(s.cycle);
241    }
242
243    #[test]
244    fn owned_by_extracted() {
245        let s = build("CREATE SEQUENCE app.users_id_seq OWNED BY app.users.id;");
246        let owner = s.owned_by.expect("owned_by present");
247        assert_eq!(owner.table.to_string(), "app.users");
248        assert_eq!(owner.column.as_str(), "id");
249    }
250
251    #[test]
252    fn owned_by_none_yields_no_owner() {
253        let s = build("CREATE SEQUENCE app.s OWNED BY NONE;");
254        assert!(s.owned_by.is_none());
255    }
256}