diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b2b3f42bb..7fc251858 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9827,18 +9827,15 @@ impl<'a> Parser<'a> { let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_expr())?; self.expect_token(&Token::RParen)?; 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_ref("STORED", self.peek_token_ref()) + ) } else if self.parse_keywords(&[Keyword::VIRTUAL]) { - Ok((GeneratedAs::Always, Some(GeneratedExpressionMode::Virtual))) + (GeneratedAs::Always, Some(GeneratedExpressionMode::Virtual)) } else { - Ok((GeneratedAs::Always, None)) - }?; + (GeneratedAs::Always, None) + }; Ok(Some(ColumnOption::Generated { generated_as: gen_as, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a7128eafd..36d58b3cf 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9663,3 +9663,26 @@ fn parse_right_deep_join_chain() { // NATURAL JOIN followed by a constrained join must stay left-associative. pg().verified_stmt("SELECT * FROM t0 NATURAL JOIN t1 INNER JOIN t2 ON true"); } + +#[test] +fn parse_create_table_generated_column_modes() { + // PostgreSQL 18 generated columns are virtual by default, so the mode keyword is + // optional and VIRTUAL may also be written explicitly. All three forms round-trip + // without gaining or losing a mode keyword. + pg().verified_stmt("CREATE TABLE t (a TEXT, b TEXT GENERATED ALWAYS AS (a))"); + pg().verified_stmt("CREATE TABLE t (a TEXT, b TEXT GENERATED ALWAYS AS (a) VIRTUAL)"); + pg().verified_stmt("CREATE TABLE t (a TEXT, b TEXT GENERATED ALWAYS AS (a) STORED)"); + + // PostgreSQL 18 pg_dump emits generated virtual columns with no mode keyword. + pg().one_statement_parses_to( + r#"CREATE TABLE users ( + first_name text NOT NULL, + last_name text NOT NULL, + name character varying(255) GENERATED ALWAYS AS (((first_name || ' '::text) || last_name)) NOT NULL +)"#, + "CREATE TABLE users (\ + first_name TEXT NOT NULL, \ + last_name TEXT NOT NULL, \ + name CHARACTER VARYING(255) GENERATED ALWAYS AS (((first_name || ' '::TEXT) || last_name)) NOT NULL)", + ); +}