diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index da2c8c9e4..3192af8bb 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -599,6 +599,7 @@ pub enum ColumnOption { generated_as: GeneratedAs, sequence_options: Option>, generation_expr: Option, + generation_expr_mode: Option, }, } @@ -639,25 +640,25 @@ impl fmt::Display for ColumnOption { generated_as, sequence_options, generation_expr, - } => match generated_as { - GeneratedAs::Always => { - write!(f, "GENERATED ALWAYS AS IDENTITY")?; - if sequence_options.is_some() { - let so = sequence_options.as_ref().unwrap(); - if !so.is_empty() { - write!(f, " (")?; - } - for sequence_option in so { - write!(f, "{sequence_option}")?; - } - if !so.is_empty() { - write!(f, " )")?; - } - } + generation_expr_mode, + } => { + if let Some(expr) = generation_expr { + let modifier = match generation_expr_mode { + None => "", + Some(GeneratedExpressionMode::Virtual) => " VIRTUAL", + Some(GeneratedExpressionMode::Stored) => " STORED", + }; + write!(f, "GENERATED ALWAYS AS ({expr}){modifier}")?; Ok(()) - } - GeneratedAs::ByDefault => { - write!(f, "GENERATED BY DEFAULT AS IDENTITY")?; + } else { + // Like Postgres - generated from sequence + let when = match generated_as { + GeneratedAs::Always => "ALWAYS", + GeneratedAs::ByDefault => "BY DEFAULT", + // ExpStored goes with an expression, handled above + GeneratedAs::ExpStored => unreachable!(), + }; + write!(f, "GENERATED {when} AS IDENTITY")?; if sequence_options.is_some() { let so = sequence_options.as_ref().unwrap(); if !so.is_empty() { @@ -672,17 +673,13 @@ impl fmt::Display for ColumnOption { } Ok(()) } - GeneratedAs::ExpStored => { - let expr = generation_expr.as_ref().unwrap(); - write!(f, "GENERATED ALWAYS AS ({expr}) STORED") - } - }, + } } } } /// `GeneratedAs`s are modifiers that follow a column option in a `generated`. -/// 'ExpStored' is PostgreSQL specific +/// 'ExpStored' is used for a column generated from an expression and stored. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -692,6 +689,16 @@ pub enum GeneratedAs { ExpStored, } +/// `GeneratedExpressionMode`s are modifiers that follow an expression in a `generated`. +/// No modifier is typically the same as Virtual. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum GeneratedExpressionMode { + Virtual, + Stored, +} + fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { struct ConstraintName<'a>(&'a Option); impl<'a> fmt::Display for ConstraintName<'a> { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3929d228b..bcee183f6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -31,8 +31,8 @@ pub use self::data_type::{ pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue}; pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, - ColumnOptionDef, GeneratedAs, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam, - ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, + ColumnOptionDef, GeneratedAs, GeneratedExpressionMode, IndexType, KeyOrIndexDisplay, Partition, + ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, }; pub use self::operator::{BinaryOperator, UnaryOperator}; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bad0470c1..079a25847 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4231,6 +4231,7 @@ impl<'a> Parser<'a> { generated_as: GeneratedAs::Always, sequence_options: Some(sequence_options), generation_expr: None, + generation_expr_mode: None, })) } else if self.parse_keywords(&[ Keyword::BY, @@ -4247,16 +4248,31 @@ impl<'a> Parser<'a> { generated_as: GeneratedAs::ByDefault, sequence_options: Some(sequence_options), generation_expr: None, + generation_expr_mode: None, })) } else if self.parse_keywords(&[Keyword::ALWAYS, Keyword::AS]) { if self.expect_token(&Token::LParen).is_ok() { let expr = self.parse_expr()?; self.expect_token(&Token::RParen)?; - let _ = self.parse_keywords(&[Keyword::STORED]); + let (gen_as, expr_mode) = if self.parse_keywords(&[Keyword::STORED]) { + Ok(( + GeneratedAs::ExpStored, + Some(GeneratedExpressionMode::Stored), + )) + } else if dialect_of!(self is PostgreSqlDialect) { + // Postgres' AS IDENTITY branches are above, this one needs STORED + self.expected("STORED", self.peek_token()) + } else if self.parse_keywords(&[Keyword::VIRTUAL]) { + Ok((GeneratedAs::Always, Some(GeneratedExpressionMode::Virtual))) + } else { + Ok((GeneratedAs::Always, None)) + }?; + Ok(Some(ColumnOption::Generated { - generated_as: GeneratedAs::ExpStored, + generated_as: gen_as, sequence_options: None, generation_expr: Some(expr), + generation_expr_mode: expr_mode, })) } else { Ok(None) diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index c80003b7d..96013df1b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -507,6 +507,18 @@ fn parse_create_table_comment_character_set() { } } +#[test] +fn parse_create_table_gencol() { + let sql_default = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2))"; + mysql_and_generic().verified_stmt(sql_default); + + let sql_virt = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2) VIRTUAL)"; + mysql_and_generic().verified_stmt(sql_virt); + + let sql_stored = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2) STORED)"; + mysql_and_generic().verified_stmt(sql_stored); +} + #[test] fn parse_quote_identifiers() { let sql = "CREATE TABLE `PRIMARY` (`BEGIN` INT PRIMARY KEY)"; diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 7cfe9422a..cc0d53b14 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -205,6 +205,18 @@ fn parse_create_sqlite_quote() { } } +#[test] +fn parse_create_table_gencol() { + let sql_default = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2))"; + sqlite_and_generic().verified_stmt(sql_default); + + let sql_virt = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2) VIRTUAL)"; + sqlite_and_generic().verified_stmt(sql_virt); + + let sql_stored = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2) STORED)"; + sqlite_and_generic().verified_stmt(sql_stored); +} + #[test] fn test_placeholder() { // In postgres, this would be the absolute value operator '@' applied to the column 'xxx' @@ -435,7 +447,6 @@ fn sqlite_with_options(options: ParserOptions) -> TestedDialects { fn sqlite_and_generic() -> TestedDialects { TestedDialects { - // we don't have a separate SQLite dialect, so test only the generic dialect for now dialects: vec![Box::new(SQLiteDialect {}), Box::new(GenericDialect {})], options: None, }