From e072e66d5991b9842c4caf5f079f16ad9a8671b5 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 2 Oct 2026 18:15:12 +0200 Subject: [PATCH 1/3] Accept `CREATE SEQUENCE` options in any order --- src/ast/mod.rs | 43 ++++++------ src/parser/mod.rs | 133 ++++++++++++++++++++---------------- tests/sqlparser_postgres.rs | 78 +++++++++++++++++++++ 3 files changed, 175 insertions(+), 79 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f38ff0f17a..5cd7fdfb05 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4810,12 +4810,8 @@ pub enum Statement { if_not_exists: bool, /// Sequence name. name: ObjectName, - /// Optional data type for the sequence. - data_type: Option, - /// Sequence options (INCREMENT, MINVALUE, etc.). + /// Sequence options, in the order they were written. sequence_options: Vec, - /// Optional `OWNED BY` target. - owned_by: Option, }, /// A `CREATE DOMAIN` statement. CreateDomain(CreateDomain), @@ -6263,32 +6259,19 @@ impl fmt::Display for Statement { temporary, if_not_exists, name, - data_type, sequence_options, - owned_by, } => { - let as_type: String = if let Some(dt) = data_type.as_ref() { - //Cannot use format!(" AS {}", dt), due to format! is not available in --target thumbv6m-none-eabi - // " AS ".to_owned() + &dt.to_string() - [" AS ", &dt.to_string()].concat() - } else { - "".to_string() - }; write!( f, - "CREATE {temporary}SEQUENCE {if_not_exists}{name}{as_type}", + "CREATE {temporary}SEQUENCE {if_not_exists}{name}", if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, temporary = if *temporary { "TEMPORARY " } else { "" }, name = name, - as_type = as_type )?; for sequence_option in sequence_options { write!(f, "{sequence_option}")?; } - if let Some(ob) = owned_by.as_ref() { - write!(f, " OWNED BY {ob}")?; - } - write!(f, "") + Ok(()) } Statement::CreateStage { or_replace, @@ -6574,14 +6557,21 @@ impl fmt::Display for Statement { /// Can use to describe options in create sequence or table column type identity /// ```sql -/// [ INCREMENT [ BY ] increment ] +/// [ AS data_type ] [ INCREMENT [ BY ] increment ] /// [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ] /// [ START [ WITH ] start ] [ CACHE cache ] [ [ NO ] CYCLE ] +/// [ OWNED BY { table_name.column_name | NONE } ] /// ``` +/// +/// The options form an unordered list, so they are stored in the order written. +/// `AS` and `OWNED BY` are only accepted by `CREATE SEQUENCE`, not by an +/// identity column. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum SequenceOptions { + /// `AS ` option. + DataType(DataType), /// `INCREMENT [BY] ` option; second value indicates presence of `BY` keyword. IncrementBy(Expr, bool), /// `MINVALUE ` or `NO MINVALUE`. @@ -6594,11 +6584,16 @@ pub enum SequenceOptions { Cache(Expr), /// `CYCLE` or `NO CYCLE` option. Cycle(bool), + /// `OWNED BY `, or `OWNED BY NONE` when no target is given. + OwnedBy(Option), } impl fmt::Display for SequenceOptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + SequenceOptions::DataType(data_type) => { + write!(f, " AS {data_type}") + } SequenceOptions::IncrementBy(increment, by) => { write!( f, @@ -6633,6 +6628,12 @@ impl fmt::Display for SequenceOptions { SequenceOptions::Cycle(no) => { write!(f, " {}CYCLE", if *no { "NO " } else { "" }) } + SequenceOptions::OwnedBy(Some(object_name)) => { + write!(f, " OWNED BY {object_name}") + } + SequenceOptions::OwnedBy(None) => { + write!(f, " OWNED BY NONE") + } } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index db54296fce..c79ddfbcfe 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -23,6 +23,7 @@ use alloc::{ }; use core::{ fmt::{self, Display}, + mem::discriminant, str::FromStr, }; #[cfg(feature = "std")] @@ -9969,7 +9970,7 @@ impl<'a> Parser<'a> { if self.parse_keywords(&[Keyword::ALWAYS, Keyword::AS, Keyword::IDENTITY]) { let mut sequence_options = vec![]; if self.expect_token(&Token::LParen).is_ok() { - sequence_options = self.parse_create_sequence_options()?; + sequence_options = self.parse_sequence_options(false)?; self.expect_token(&Token::RParen)?; } Ok(Some(ColumnOption::Generated { @@ -9987,7 +9988,7 @@ impl<'a> Parser<'a> { ]) { let mut sequence_options = vec![]; if self.expect_token(&Token::LParen).is_ok() { - sequence_options = self.parse_create_sequence_options()?; + sequence_options = self.parse_sequence_options(false)?; self.expect_token(&Token::RParen)?; } Ok(Some(ColumnOption::Generated { @@ -11145,7 +11146,7 @@ impl<'a> Parser<'a> { if self.peek_token_ref().token == Token::LParen { self.expect_token(&Token::LParen)?; - sequence_options = Some(self.parse_create_sequence_options()?); + sequence_options = Some(self.parse_sequence_options(false)?); self.expect_token(&Token::RParen)?; } @@ -20650,72 +20651,88 @@ impl<'a> Parser<'a> { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); //name let name = self.parse_object_name(false)?; - //[ AS data_type ] - let mut data_type: Option = None; - if self.parse_keywords(&[Keyword::AS]) { - data_type = Some(self.parse_data_type()?) - } - let sequence_options = self.parse_create_sequence_options()?; - // [ OWNED BY { table_name.column_name | NONE } ] - let owned_by = if self.parse_keywords(&[Keyword::OWNED, Keyword::BY]) { - if self.parse_keywords(&[Keyword::NONE]) { - Some(ObjectName::from(vec![Ident::new("NONE")])) - } else { - Some(self.parse_object_name(false)?) - } - } else { - None - }; + let sequence_options = self.parse_sequence_options(true)?; Ok(Statement::CreateSequence { temporary, if_not_exists, name, - data_type, sequence_options, - owned_by, }) } - fn parse_create_sequence_options(&mut self) -> Result, ParserError> { - let mut sequence_options = vec![]; - //[ INCREMENT [ BY ] increment ] - if self.parse_keywords(&[Keyword::INCREMENT]) { - if self.parse_keywords(&[Keyword::BY]) { - sequence_options.push(SequenceOptions::IncrementBy(self.parse_number()?, true)); + /// Parse the sequence options shared by `CREATE SEQUENCE` and identity + /// columns. The options form an unordered list, each allowed at most once. + /// + /// `AS ` and `OWNED BY` are options of `CREATE SEQUENCE` only, so + /// `allow_type_and_owner` gates them off for an identity column. + fn parse_sequence_options( + &mut self, + allow_type_and_owner: bool, + ) -> Result, ParserError> { + let mut sequence_options: Vec = vec![]; + loop { + let (option, name) = if self.parse_keyword(Keyword::INCREMENT) { + //[ INCREMENT [ BY ] increment ] + let by = self.parse_keyword(Keyword::BY); + ( + SequenceOptions::IncrementBy(self.parse_number()?, by), + "INCREMENT", + ) + } else if self.parse_keyword(Keyword::MINVALUE) { + //[ MINVALUE minvalue | NO MINVALUE ] + ( + SequenceOptions::MinValue(Some(self.parse_number()?)), + "MINVALUE | NO MINVALUE", + ) + } else if self.parse_keywords(&[Keyword::NO, Keyword::MINVALUE]) { + (SequenceOptions::MinValue(None), "MINVALUE | NO MINVALUE") + } else if self.parse_keyword(Keyword::MAXVALUE) { + //[ MAXVALUE maxvalue | NO MAXVALUE ] + ( + SequenceOptions::MaxValue(Some(self.parse_number()?)), + "MAXVALUE | NO MAXVALUE", + ) + } else if self.parse_keywords(&[Keyword::NO, Keyword::MAXVALUE]) { + (SequenceOptions::MaxValue(None), "MAXVALUE | NO MAXVALUE") + } else if self.parse_keyword(Keyword::START) { + //[ START [ WITH ] start ] + let with = self.parse_keyword(Keyword::WITH); + ( + SequenceOptions::StartWith(self.parse_number()?, with), + "START", + ) + } else if self.parse_keyword(Keyword::CACHE) { + //[ CACHE cache ] + (SequenceOptions::Cache(self.parse_number()?), "CACHE") + } else if self.parse_keywords(&[Keyword::NO, Keyword::CYCLE]) { + // [ [ NO ] CYCLE ] + (SequenceOptions::Cycle(true), "CYCLE | NO CYCLE") + } else if self.parse_keyword(Keyword::CYCLE) { + (SequenceOptions::Cycle(false), "CYCLE | NO CYCLE") + } else if allow_type_and_owner && self.parse_keyword(Keyword::AS) { + //[ AS data_type ] + (SequenceOptions::DataType(self.parse_data_type()?), "AS") + } else if allow_type_and_owner && self.parse_keywords(&[Keyword::OWNED, Keyword::BY]) { + // [ OWNED BY { table_name.column_name | NONE } ] + let owner = if self.parse_keyword(Keyword::NONE) { + None + } else { + Some(self.parse_object_name(false)?) + }; + (SequenceOptions::OwnedBy(owner), "OWNED BY") } else { - sequence_options.push(SequenceOptions::IncrementBy(self.parse_number()?, false)); - } - } - //[ MINVALUE minvalue | NO MINVALUE ] - if self.parse_keyword(Keyword::MINVALUE) { - sequence_options.push(SequenceOptions::MinValue(Some(self.parse_number()?))); - } else if self.parse_keywords(&[Keyword::NO, Keyword::MINVALUE]) { - sequence_options.push(SequenceOptions::MinValue(None)); - } - //[ MAXVALUE maxvalue | NO MAXVALUE ] - if self.parse_keywords(&[Keyword::MAXVALUE]) { - sequence_options.push(SequenceOptions::MaxValue(Some(self.parse_number()?))); - } else if self.parse_keywords(&[Keyword::NO, Keyword::MAXVALUE]) { - sequence_options.push(SequenceOptions::MaxValue(None)); - } + break; + }; - //[ START [ WITH ] start ] - if self.parse_keywords(&[Keyword::START]) { - if self.parse_keywords(&[Keyword::WITH]) { - sequence_options.push(SequenceOptions::StartWith(self.parse_number()?, true)); - } else { - sequence_options.push(SequenceOptions::StartWith(self.parse_number()?, false)); + if sequence_options + .iter() + .any(|seen| discriminant(seen) == discriminant(&option)) + { + return Err(ParserError::ParserError(format!( + "{name} specified more than once" + ))); } - } - //[ CACHE cache ] - if self.parse_keywords(&[Keyword::CACHE]) { - sequence_options.push(SequenceOptions::Cache(self.parse_number()?)); - } - // [ [ NO ] CYCLE ] - if self.parse_keywords(&[Keyword::NO, Keyword::CYCLE]) { - sequence_options.push(SequenceOptions::Cycle(true)); - } else if self.parse_keywords(&[Keyword::CYCLE]) { - sequence_options.push(SequenceOptions::Cycle(false)); + sequence_options.push(option); } Ok(sequence_options) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1e00288673..ec10fd4cf5 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10202,3 +10202,81 @@ fn parse_string_literal_alias_rejected() { } pg().verified_stmt(r#"SELECT a AS "x" FROM t AS "y""#); } + +#[test] +fn parse_create_sequence_options_in_any_order() { + // PostgreSQL treats the sequence options as an unordered list, so every + // permutation below is accepted and round-trips in the order written. + pg().verified_stmt("CREATE SEQUENCE sa START WITH 1 INCREMENT BY 1"); + pg().verified_stmt("CREATE SEQUENCE sb INCREMENT BY 1 START WITH 1"); + pg().verified_stmt("CREATE SEQUENCE sc CACHE 1 START WITH 1"); + pg().verified_stmt("CREATE SEQUENCE sd CYCLE INCREMENT BY 2"); + pg().verified_stmt("CREATE SEQUENCE se NO MAXVALUE NO MINVALUE NO CYCLE INCREMENT 1"); + pg().verified_stmt( + "CREATE SEQUENCE sf AS INTEGER START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1", + ); + // `AS` and `OWNED BY` are options too, so they interleave with the rest. + pg().verified_stmt("CREATE SEQUENCE sg START WITH 1 AS BIGINT"); + pg().verified_stmt("CREATE SEQUENCE sh OWNED BY t.c START WITH 5"); + pg().verified_stmt("CREATE SEQUENCE si OWNED BY NONE INCREMENT BY 2"); + pg().verified_stmt("CREATE SEQUENCE sj CACHE 1 AS BIGINT OWNED BY a.b START WITH 3"); + + // This is what `pg_dump -s` emits for a table with a SERIAL column. + pg().one_statement_parses_to( + r#"CREATE SEQUENCE public.accounts_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1"#, + "CREATE SEQUENCE public.accounts_id_seq AS INTEGER START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1", + ); + + // The parsed options keep the order they were written in, and `OWNED BY NONE` + // is distinct from being owned by something called `NONE`. + let Statement::CreateSequence { + sequence_options, .. + } = pg().verified_stmt("CREATE SEQUENCE sk CACHE 1 OWNED BY NONE AS BIGINT INCREMENT BY 3") + else { + panic!("expected a CREATE SEQUENCE statement"); + }; + assert!(matches!( + sequence_options.as_slice(), + [ + SequenceOptions::Cache(_), + SequenceOptions::OwnedBy(None), + SequenceOptions::DataType(DataType::BigInt(None)), + SequenceOptions::IncrementBy(_, true), + ] + )); + + // Each option may still appear only once. + for (sql, expected) in [ + ( + "CREATE SEQUENCE s START WITH 1 START 2", + "START specified more than once", + ), + ( + "CREATE SEQUENCE s MINVALUE 1 NO MINVALUE", + "MINVALUE | NO MINVALUE specified more than once", + ), + ( + "CREATE SEQUENCE s CYCLE NO CYCLE", + "CYCLE | NO CYCLE specified more than once", + ), + ( + "CREATE SEQUENCE s AS INT AS BIGINT", + "AS specified more than once", + ), + ( + "CREATE SEQUENCE s OWNED BY a.b OWNED BY NONE", + "OWNED BY specified more than once", + ), + ] { + assert_eq!( + pg().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + ); + } +} From e4872b51f5bef9e54a90f10e8b75b58f72900abf Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Mon, 19 Oct 2026 20:10:23 +0200 Subject: [PATCH 2/3] Treat only a bare NONE as no sequence owner and test the options in every dialect --- src/parser/mod.rs | 17 ++++-- tests/sqlparser_common.rs | 116 ++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 78 ------------------------ 3 files changed, 127 insertions(+), 84 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c79ddfbcfe..a2ce6b45e2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -20714,12 +20714,17 @@ impl<'a> Parser<'a> { (SequenceOptions::DataType(self.parse_data_type()?), "AS") } else if allow_type_and_owner && self.parse_keywords(&[Keyword::OWNED, Keyword::BY]) { // [ OWNED BY { table_name.column_name | NONE } ] - let owner = if self.parse_keyword(Keyword::NONE) { - None - } else { - Some(self.parse_object_name(false)?) - }; - (SequenceOptions::OwnedBy(owner), "OWNED BY") + // Only the single bare word `NONE` means no owner, `none.c` is a column. + let owner = self.parse_object_name(false)?; + let is_none = matches!( + owner.0.as_slice(), + [ObjectNamePart::Identifier(ident)] + if ident.quote_style.is_none() && ident.value.eq_ignore_ascii_case("NONE") + ); + ( + SequenceOptions::OwnedBy((!is_none).then_some(owner)), + "OWNED BY", + ) } else { break; }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 025c2cf207..e759142aba 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20803,3 +20803,119 @@ fn display_nested_unary_minus_in_linear_time() { .expect("display should not take exponential time"); assert_eq!(printed, sql); } + +#[test] +fn parse_sequence_options_in_any_order() { + // The sequence options form an unordered list, so every permutation below + // is accepted and round-trips in the order written. + let dialects = all_dialects(); + for sql in [ + "CREATE SEQUENCE sa START WITH 1 INCREMENT BY 1", + "CREATE SEQUENCE sb INCREMENT BY 1 START WITH 1", + "CREATE SEQUENCE sc CACHE 1 START WITH 1", + "CREATE SEQUENCE sd CYCLE INCREMENT BY 2", + "CREATE SEQUENCE se NO MAXVALUE NO MINVALUE NO CYCLE INCREMENT 1", + "CREATE SEQUENCE sf AS INTEGER START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1", + "CREATE SEQUENCE sg START WITH 1 AS BIGINT", + "CREATE SEQUENCE sh OWNED BY t.c START WITH 5", + "CREATE SEQUENCE si OWNED BY NONE INCREMENT BY 2", + "CREATE SEQUENCE sj CACHE 1 AS BIGINT OWNED BY a.b START WITH 3", + "CREATE SEQUENCE sk OWNED BY none.c", + "CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY ( START WITH 10 INCREMENT BY 1 CYCLE ))", + "ALTER TABLE t ALTER COLUMN id ADD GENERATED AS IDENTITY ( CACHE 2 INCREMENT 1 )", + ] { + dialects.verified_stmt(sql); + } + + // This is what `pg_dump -s` emits for a table with a SERIAL column. + dialects.one_statement_parses_to( + r#"CREATE SEQUENCE public.accounts_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1"#, + "CREATE SEQUENCE public.accounts_id_seq AS INTEGER START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1", + ); + + // The parsed options keep the order they were written in, and `OWNED BY NONE` + // is distinct from being owned by something called `NONE`. + let Statement::CreateSequence { + sequence_options, .. + } = dialects.verified_stmt("CREATE SEQUENCE sk CACHE 1 OWNED BY NONE AS BIGINT INCREMENT BY 3") + else { + panic!("expected a CREATE SEQUENCE statement"); + }; + assert!(matches!( + sequence_options.as_slice(), + [ + SequenceOptions::Cache(_), + SequenceOptions::OwnedBy(None), + SequenceOptions::DataType(DataType::BigInt(None)), + SequenceOptions::IncrementBy(_, true), + ] + )); + + for (sql, expected) in [ + // Each option may appear only once. + ( + "CREATE SEQUENCE s START WITH 1 START 2", + "START specified more than once", + ), + ( + "CREATE SEQUENCE s INCREMENT 1 INCREMENT BY 2", + "INCREMENT specified more than once", + ), + ( + "CREATE SEQUENCE s MINVALUE 1 NO MINVALUE", + "MINVALUE | NO MINVALUE specified more than once", + ), + ( + "CREATE SEQUENCE s MAXVALUE 1 NO MAXVALUE", + "MAXVALUE | NO MAXVALUE specified more than once", + ), + ( + "CREATE SEQUENCE s CACHE 1 CACHE 2", + "CACHE specified more than once", + ), + ( + "CREATE SEQUENCE s CYCLE NO CYCLE", + "CYCLE | NO CYCLE specified more than once", + ), + ( + "CREATE SEQUENCE s AS INT AS BIGINT", + "AS specified more than once", + ), + ( + "CREATE SEQUENCE s OWNED BY a.b OWNED BY NONE", + "OWNED BY specified more than once", + ), + ( + "CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY (START 1 START 2))", + "START specified more than once", + ), + ( + "CREATE SEQUENCE s AS", + "Expected: a data type name, found: EOF", + ), + ( + "CREATE SEQUENCE s OWNED BY", + "Expected: identifier, found: EOF", + ), + // Identity columns take the shared list without `AS` and `OWNED BY`. + ( + "CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY (AS BIGINT))", + "Expected: ), found: AS", + ), + ( + "CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY (OWNED BY t.id))", + "Expected: ), found: OWNED", + ), + ] { + assert_eq!( + dialects.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + ); + } +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ec10fd4cf5..1e00288673 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10202,81 +10202,3 @@ fn parse_string_literal_alias_rejected() { } pg().verified_stmt(r#"SELECT a AS "x" FROM t AS "y""#); } - -#[test] -fn parse_create_sequence_options_in_any_order() { - // PostgreSQL treats the sequence options as an unordered list, so every - // permutation below is accepted and round-trips in the order written. - pg().verified_stmt("CREATE SEQUENCE sa START WITH 1 INCREMENT BY 1"); - pg().verified_stmt("CREATE SEQUENCE sb INCREMENT BY 1 START WITH 1"); - pg().verified_stmt("CREATE SEQUENCE sc CACHE 1 START WITH 1"); - pg().verified_stmt("CREATE SEQUENCE sd CYCLE INCREMENT BY 2"); - pg().verified_stmt("CREATE SEQUENCE se NO MAXVALUE NO MINVALUE NO CYCLE INCREMENT 1"); - pg().verified_stmt( - "CREATE SEQUENCE sf AS INTEGER START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1", - ); - // `AS` and `OWNED BY` are options too, so they interleave with the rest. - pg().verified_stmt("CREATE SEQUENCE sg START WITH 1 AS BIGINT"); - pg().verified_stmt("CREATE SEQUENCE sh OWNED BY t.c START WITH 5"); - pg().verified_stmt("CREATE SEQUENCE si OWNED BY NONE INCREMENT BY 2"); - pg().verified_stmt("CREATE SEQUENCE sj CACHE 1 AS BIGINT OWNED BY a.b START WITH 3"); - - // This is what `pg_dump -s` emits for a table with a SERIAL column. - pg().one_statement_parses_to( - r#"CREATE SEQUENCE public.accounts_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1"#, - "CREATE SEQUENCE public.accounts_id_seq AS INTEGER START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1", - ); - - // The parsed options keep the order they were written in, and `OWNED BY NONE` - // is distinct from being owned by something called `NONE`. - let Statement::CreateSequence { - sequence_options, .. - } = pg().verified_stmt("CREATE SEQUENCE sk CACHE 1 OWNED BY NONE AS BIGINT INCREMENT BY 3") - else { - panic!("expected a CREATE SEQUENCE statement"); - }; - assert!(matches!( - sequence_options.as_slice(), - [ - SequenceOptions::Cache(_), - SequenceOptions::OwnedBy(None), - SequenceOptions::DataType(DataType::BigInt(None)), - SequenceOptions::IncrementBy(_, true), - ] - )); - - // Each option may still appear only once. - for (sql, expected) in [ - ( - "CREATE SEQUENCE s START WITH 1 START 2", - "START specified more than once", - ), - ( - "CREATE SEQUENCE s MINVALUE 1 NO MINVALUE", - "MINVALUE | NO MINVALUE specified more than once", - ), - ( - "CREATE SEQUENCE s CYCLE NO CYCLE", - "CYCLE | NO CYCLE specified more than once", - ), - ( - "CREATE SEQUENCE s AS INT AS BIGINT", - "AS specified more than once", - ), - ( - "CREATE SEQUENCE s OWNED BY a.b OWNED BY NONE", - "OWNED BY specified more than once", - ), - ] { - assert_eq!( - pg().parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError(expected.to_string()), - ); - } -} From eba3aef5a58492d4778cf44aa4e1898e54faa4ac Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 12:51:14 +0200 Subject: [PATCH 3/3] Cover sequence-option numeric error propagation --- tests/sqlparser_common.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e759142aba..7c361e0f2b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20919,3 +20919,35 @@ fn parse_sequence_options_in_any_order() { ); } } + +#[test] +fn sequence_options_reject_non_numeric_values() { + let dialects = all_dialects(); + for (sql, expected) in [ + ( + "CREATE SEQUENCE s MINVALUE x", + "Expected: a concrete value, found: x", + ), + ( + "CREATE SEQUENCE s MAXVALUE x", + "Expected: a concrete value, found: x", + ), + ( + "CREATE SEQUENCE s START WITH x", + "Expected: a concrete value, found: x", + ), + ( + "CREATE SEQUENCE s CACHE x", + "Expected: a concrete value, found: x", + ), + ( + "CREATE TABLE t (id INT GENERATED BY DEFAULT AS IDENTITY (MINVALUE x))", + "Expected: a concrete value, found: x", + ), + ] { + assert_eq!( + dialects.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + ); + } +}