From 3f25d96ccd9c805c4fee3eb758bcb0bf06249902 Mon Sep 17 00:00:00 2001 From: Ting-Hong Shieh <32212900+ting-hong-shieh@users.noreply.github.com> Date: Mon, 17 Aug 2026 06:33:50 +0800 Subject: [PATCH] Postgres: Accept generated columns with no mode keyword or VIRTUAL PostgreSqlDialect required STORED after GENERATED ALWAYS AS (expr), so it rejected both the omitted mode and an explicit VIRTUAL. PostgreSQL 18 documents the grammar as GENERATED ALWAYS AS ( generation_expr ) [ STORED | VIRTUAL ], with VIRTUAL as the default when the keyword is omitted. Remove the PostgreSqlDialect special case so the dialect falls back to the shared handling, which parses both forms and round-trips them unchanged. --- src/parser/mod.rs | 13 +++++-------- tests/sqlparser_postgres.rs | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6fd25b9eb..908ff065b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9867,18 +9867,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 23e20c718..0c57b6da6 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10042,3 +10042,26 @@ fn parse_bitstring_literal_escaping() { pg_and_generic().verified_stmt("SELECT B''''"); pg_and_generic().verified_stmt("SELECT B'it''s'"); } + +#[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)", + ); +}