From 580880ed528a2f4dcddd26800dd66688920bc357 Mon Sep 17 00:00:00 2001 From: "Guan-Ming (Wesley) Chiu" <105915352+guan404ming@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:14:43 +0800 Subject: [PATCH 1/2] Support PostgreSQL ANALYZE with optional table and column Signed-off-by: Guan-Ming (Wesley) Chiu <105915352+guan404ming@users.noreply.github.com> --- src/ast/mod.rs | 34 +++++++++++++++++++--------------- src/ast/spans.rs | 4 +++- src/parser/mod.rs | 10 +++++++++- tests/sqlparser_postgres.rs | 25 +++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f255e5f3f..f5bc5f7a9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3361,19 +3361,24 @@ impl Display for ExceptionWhen { } } -/// ANALYZE TABLE statement (Hive-specific) +/// ANALYZE statement +/// +/// Supported syntax varies by dialect: +/// - Hive: `ANALYZE TABLE t [PARTITION (...)] COMPUTE STATISTICS [NOSCAN] [FOR COLUMNS [col1, ...]] [CACHE METADATA]` +/// - PostgreSQL: `ANALYZE [VERBOSE] [t [(col1, ...)]]` +/// - General: `ANALYZE [TABLE] t` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Analyze { #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - /// Name of the table to analyze. - pub table_name: ObjectName, + /// Name of the table to analyze. `None` for bare `ANALYZE`. + pub table_name: Option, /// Optional partition expressions to restrict the analysis. pub partitions: Option>, - /// `true` when analyzing specific columns. + /// `true` when analyzing specific columns (Hive `FOR COLUMNS` syntax). pub for_columns: bool, - /// Columns to analyze when `for_columns` is `true`. + /// Columns to analyze. pub columns: Vec, /// Whether to cache metadata before analyzing. pub cache_metadata: bool, @@ -3387,22 +3392,21 @@ pub struct Analyze { impl fmt::Display for Analyze { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "ANALYZE{}{table_name}", + write!(f, "ANALYZE")?; + if let Some(ref table_name) = self.table_name { if self.has_table_keyword { - " TABLE " - } else { - " " - }, - table_name = self.table_name - )?; + write!(f, " TABLE")?; + } + write!(f, " {table_name}")?; + } + if !self.for_columns && !self.columns.is_empty() { + write!(f, " ({})", display_comma_separated(&self.columns))?; + } if let Some(ref parts) = self.partitions { if !parts.is_empty() { write!(f, " PARTITION ({})", display_comma_separated(parts))?; } } - if self.compute_statistics { write!(f, " COMPUTE STATISTICS")?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 60c983fa1..28dad6186 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -841,7 +841,9 @@ impl Spanned for ConstraintCharacteristics { impl Spanned for Analyze { fn span(&self) -> Span { union_spans( - core::iter::once(self.table_name.span()) + self.table_name + .iter() + .map(|t| t.span()) .chain( self.partitions .iter() diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 276311431..13be249dc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1195,13 +1195,21 @@ impl<'a> Parser<'a> { /// Parse `ANALYZE` statement. pub fn parse_analyze(&mut self) -> Result { let has_table_keyword = self.parse_keyword(Keyword::TABLE); - let table_name = self.parse_object_name(false)?; + let table_name = self + .maybe_parse(|parser| parser.parse_object_name(false))?; let mut for_columns = false; let mut cache_metadata = false; let mut noscan = false; let mut partitions = None; let mut compute_statistics = false; let mut columns = vec![]; + + // PostgreSQL syntax: ANALYZE t (col1, col2) + if table_name.is_some() && self.consume_token(&Token::LParen) { + columns = self.parse_comma_separated(|p| p.parse_identifier())?; + self.expect_token(&Token::RParen)?; + } + loop { match self.parse_one_of_keywords(&[ Keyword::PARTITION, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 54e9ee0c0..f1329c55e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -8501,3 +8501,28 @@ fn parse_create_table_partition_of_errors() { "Expected error about empty TO list, got: {err}" ); } + +#[test] +fn parse_pg_analyze() { + // Bare ANALYZE + pg_and_generic().verified_stmt("ANALYZE"); + + // ANALYZE with table name + pg_and_generic().verified_stmt("ANALYZE t"); + + // ANALYZE with column specification + pg_and_generic().verified_stmt("ANALYZE t (col1, col2)"); + + // Verify AST for column specification + let stmt = pg().verified_stmt("ANALYZE t (col1, col2)"); + match &stmt { + Statement::Analyze(analyze) => { + assert_eq!(analyze.table_name.as_ref().unwrap().to_string(), "t"); + assert_eq!(analyze.columns.len(), 2); + assert_eq!(analyze.columns[0].to_string(), "col1"); + assert_eq!(analyze.columns[1].to_string(), "col2"); + assert!(!analyze.for_columns); + } + _ => panic!("Expected Analyze, got: {stmt:?}"), + } +} From 116e992e9695f338914df3ec00e6c9e18cf14088 Mon Sep 17 00:00:00 2001 From: "Guan-Ming (Wesley) Chiu" <105915352+guan404ming@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:22:08 +0800 Subject: [PATCH 2/2] Fix format Signed-off-by: Guan-Ming (Wesley) Chiu <105915352+guan404ming@users.noreply.github.com> --- src/parser/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 13be249dc..dae29a445 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1195,8 +1195,7 @@ impl<'a> Parser<'a> { /// Parse `ANALYZE` statement. pub fn parse_analyze(&mut self) -> Result { let has_table_keyword = self.parse_keyword(Keyword::TABLE); - let table_name = self - .maybe_parse(|parser| parser.parse_object_name(false))?; + let table_name = self.maybe_parse(|parser| parser.parse_object_name(false))?; let mut for_columns = false; let mut cache_metadata = false; let mut noscan = false;