Skip to content

Commit

Permalink
Preserve presence/absence of VIRTUAL for generated columns
Browse files Browse the repository at this point in the history
  • Loading branch information
takluyver committed Nov 22, 2023
1 parent f0d0f5b commit 6d69a43
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 38 deletions.
61 changes: 31 additions & 30 deletions src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@ pub enum ColumnOption {
generated_as: GeneratedAs,
sequence_options: Option<Vec<SequenceOptions>>,
generation_expr: Option<Expr>,
generation_expr_mode: Option<GeneratedExpressionMode>,
},
}

Expand Down Expand Up @@ -639,31 +640,25 @@ impl fmt::Display for ColumnOption {
generated_as,
sequence_options,
generation_expr,
} => match generated_as {
GeneratedAs::Always => {
if let Some(expr) = generation_expr {
// Like SQLite, MySQL - expr evaluated on read
write!(f, "GENERATED ALWAYS AS ({expr}) VIRTUAL")
} else {
// Like Postgres - generated from sequence
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, " )")?;
}
}
Ok(())
}
}
GeneratedAs::ByDefault => {
write!(f, "GENERATED BY DEFAULT AS IDENTITY")?;
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(())
} 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() {
Expand All @@ -678,11 +673,7 @@ impl fmt::Display for ColumnOption {
}
Ok(())
}
GeneratedAs::ExpStored => {
let expr = generation_expr.as_ref().unwrap();
write!(f, "GENERATED ALWAYS AS ({expr}) STORED")
}
},
}
}
}
}
Expand All @@ -698,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<Ident>) -> impl fmt::Display + '_ {
struct ConstraintName<'a>(&'a Option<Ident>);
impl<'a> fmt::Display for ConstraintName<'a> {
Expand Down
4 changes: 2 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
15 changes: 11 additions & 4 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -4247,25 +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 gen_as = if self.parse_keywords(&[Keyword::STORED]) {
Ok(GeneratedAs::ExpStored)
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 {
let _ = self.parse_keywords(&[Keyword::VIRTUAL]);
Ok(GeneratedAs::Always)
Ok((GeneratedAs::Always, None))
}?;

Ok(Some(ColumnOption::Generated {
generated_as: gen_as,
sequence_options: None,
generation_expr: Some(expr),
generation_expr_mode: expr_mode,
}))
} else {
Ok(None)
Expand Down
2 changes: 2 additions & 0 deletions src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ impl TestedDialects {

let only_statement = statements.pop().unwrap();
if !canonical.is_empty() {
println!("Canonical: {canonical}");
println!("Reformed: {only_statement}");
assert_eq!(canonical, only_statement.to_string())
}
only_statement
Expand Down
5 changes: 4 additions & 1 deletion tests/sqlparser_mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
//! is also tested (on the inputs it can handle).
use matches::assert_matches;
use sqlparser;
use sqlparser::ast::Expr;
use sqlparser::ast::Value;
use sqlparser::ast::*;
Expand Down Expand Up @@ -510,8 +511,10 @@ 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().one_statement_parses_to(sql_default, sql_virt);
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);
Expand Down
4 changes: 3 additions & 1 deletion tests/sqlparser_sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,10 @@ 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().one_statement_parses_to(sql_default, sql_virt);
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);
Expand Down

0 comments on commit 6d69a43

Please sign in to comment.