Skip to content

Commit 640b939

Browse files
authored
Add support for generated virtual columns with expression (#1051)
1 parent 541d684 commit 640b939

File tree

5 files changed

+75
-29
lines changed

5 files changed

+75
-29
lines changed

src/ast/ddl.rs

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@ pub enum ColumnOption {
599599
generated_as: GeneratedAs,
600600
sequence_options: Option<Vec<SequenceOptions>>,
601601
generation_expr: Option<Expr>,
602+
generation_expr_mode: Option<GeneratedExpressionMode>,
602603
},
603604
}
604605

@@ -639,25 +640,25 @@ impl fmt::Display for ColumnOption {
639640
generated_as,
640641
sequence_options,
641642
generation_expr,
642-
} => match generated_as {
643-
GeneratedAs::Always => {
644-
write!(f, "GENERATED ALWAYS AS IDENTITY")?;
645-
if sequence_options.is_some() {
646-
let so = sequence_options.as_ref().unwrap();
647-
if !so.is_empty() {
648-
write!(f, " (")?;
649-
}
650-
for sequence_option in so {
651-
write!(f, "{sequence_option}")?;
652-
}
653-
if !so.is_empty() {
654-
write!(f, " )")?;
655-
}
656-
}
643+
generation_expr_mode,
644+
} => {
645+
if let Some(expr) = generation_expr {
646+
let modifier = match generation_expr_mode {
647+
None => "",
648+
Some(GeneratedExpressionMode::Virtual) => " VIRTUAL",
649+
Some(GeneratedExpressionMode::Stored) => " STORED",
650+
};
651+
write!(f, "GENERATED ALWAYS AS ({expr}){modifier}")?;
657652
Ok(())
658-
}
659-
GeneratedAs::ByDefault => {
660-
write!(f, "GENERATED BY DEFAULT AS IDENTITY")?;
653+
} else {
654+
// Like Postgres - generated from sequence
655+
let when = match generated_as {
656+
GeneratedAs::Always => "ALWAYS",
657+
GeneratedAs::ByDefault => "BY DEFAULT",
658+
// ExpStored goes with an expression, handled above
659+
GeneratedAs::ExpStored => unreachable!(),
660+
};
661+
write!(f, "GENERATED {when} AS IDENTITY")?;
661662
if sequence_options.is_some() {
662663
let so = sequence_options.as_ref().unwrap();
663664
if !so.is_empty() {
@@ -672,17 +673,13 @@ impl fmt::Display for ColumnOption {
672673
}
673674
Ok(())
674675
}
675-
GeneratedAs::ExpStored => {
676-
let expr = generation_expr.as_ref().unwrap();
677-
write!(f, "GENERATED ALWAYS AS ({expr}) STORED")
678-
}
679-
},
676+
}
680677
}
681678
}
682679
}
683680

684681
/// `GeneratedAs`s are modifiers that follow a column option in a `generated`.
685-
/// 'ExpStored' is PostgreSQL specific
682+
/// 'ExpStored' is used for a column generated from an expression and stored.
686683
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
687684
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
688685
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
@@ -692,6 +689,16 @@ pub enum GeneratedAs {
692689
ExpStored,
693690
}
694691

692+
/// `GeneratedExpressionMode`s are modifiers that follow an expression in a `generated`.
693+
/// No modifier is typically the same as Virtual.
694+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
695+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
696+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
697+
pub enum GeneratedExpressionMode {
698+
Virtual,
699+
Stored,
700+
}
701+
695702
fn display_constraint_name(name: &'_ Option<Ident>) -> impl fmt::Display + '_ {
696703
struct ConstraintName<'a>(&'a Option<Ident>);
697704
impl<'a> fmt::Display for ConstraintName<'a> {

src/ast/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ pub use self::data_type::{
3131
pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue};
3232
pub use self::ddl::{
3333
AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption,
34-
ColumnOptionDef, GeneratedAs, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam,
35-
ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef,
34+
ColumnOptionDef, GeneratedAs, GeneratedExpressionMode, IndexType, KeyOrIndexDisplay, Partition,
35+
ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef,
3636
UserDefinedTypeRepresentation,
3737
};
3838
pub use self::operator::{BinaryOperator, UnaryOperator};

src/parser/mod.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4299,6 +4299,7 @@ impl<'a> Parser<'a> {
42994299
generated_as: GeneratedAs::Always,
43004300
sequence_options: Some(sequence_options),
43014301
generation_expr: None,
4302+
generation_expr_mode: None,
43024303
}))
43034304
} else if self.parse_keywords(&[
43044305
Keyword::BY,
@@ -4315,16 +4316,31 @@ impl<'a> Parser<'a> {
43154316
generated_as: GeneratedAs::ByDefault,
43164317
sequence_options: Some(sequence_options),
43174318
generation_expr: None,
4319+
generation_expr_mode: None,
43184320
}))
43194321
} else if self.parse_keywords(&[Keyword::ALWAYS, Keyword::AS]) {
43204322
if self.expect_token(&Token::LParen).is_ok() {
43214323
let expr = self.parse_expr()?;
43224324
self.expect_token(&Token::RParen)?;
4323-
let _ = self.parse_keywords(&[Keyword::STORED]);
4325+
let (gen_as, expr_mode) = if self.parse_keywords(&[Keyword::STORED]) {
4326+
Ok((
4327+
GeneratedAs::ExpStored,
4328+
Some(GeneratedExpressionMode::Stored),
4329+
))
4330+
} else if dialect_of!(self is PostgreSqlDialect) {
4331+
// Postgres' AS IDENTITY branches are above, this one needs STORED
4332+
self.expected("STORED", self.peek_token())
4333+
} else if self.parse_keywords(&[Keyword::VIRTUAL]) {
4334+
Ok((GeneratedAs::Always, Some(GeneratedExpressionMode::Virtual)))
4335+
} else {
4336+
Ok((GeneratedAs::Always, None))
4337+
}?;
4338+
43244339
Ok(Some(ColumnOption::Generated {
4325-
generated_as: GeneratedAs::ExpStored,
4340+
generated_as: gen_as,
43264341
sequence_options: None,
43274342
generation_expr: Some(expr),
4343+
generation_expr_mode: expr_mode,
43284344
}))
43294345
} else {
43304346
Ok(None)

tests/sqlparser_mysql.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,18 @@ fn parse_create_table_comment_character_set() {
507507
}
508508
}
509509

510+
#[test]
511+
fn parse_create_table_gencol() {
512+
let sql_default = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2))";
513+
mysql_and_generic().verified_stmt(sql_default);
514+
515+
let sql_virt = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2) VIRTUAL)";
516+
mysql_and_generic().verified_stmt(sql_virt);
517+
518+
let sql_stored = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2) STORED)";
519+
mysql_and_generic().verified_stmt(sql_stored);
520+
}
521+
510522
#[test]
511523
fn parse_quote_identifiers() {
512524
let sql = "CREATE TABLE `PRIMARY` (`BEGIN` INT PRIMARY KEY)";

tests/sqlparser_sqlite.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,18 @@ fn parse_create_sqlite_quote() {
205205
}
206206
}
207207

208+
#[test]
209+
fn parse_create_table_gencol() {
210+
let sql_default = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2))";
211+
sqlite_and_generic().verified_stmt(sql_default);
212+
213+
let sql_virt = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2) VIRTUAL)";
214+
sqlite_and_generic().verified_stmt(sql_virt);
215+
216+
let sql_stored = "CREATE TABLE t1 (a INT, b INT GENERATED ALWAYS AS (a * 2) STORED)";
217+
sqlite_and_generic().verified_stmt(sql_stored);
218+
}
219+
208220
#[test]
209221
fn test_placeholder() {
210222
// 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 {
435447

436448
fn sqlite_and_generic() -> TestedDialects {
437449
TestedDialects {
438-
// we don't have a separate SQLite dialect, so test only the generic dialect for now
439450
dialects: vec![Box::new(SQLiteDialect {}), Box::new(GenericDialect {})],
440451
options: None,
441452
}

0 commit comments

Comments
 (0)