This is an automated email from the ASF dual-hosted git repository. github-bot pushed a commit to branch gh-readonly-queue/main/pr-2187-23acd2376698badf0d7f4e5ed818ff606b5357a4 in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
commit 0924f3a9b28cf79e7ef49819dabc719523f9aa8b Author: Guan-Ming (Wesley) Chiu <[email protected]> AuthorDate: Fri Feb 6 23:41:38 2026 +0800 PostgreSQL: Support PostgreSQL ANALYZE with optional table and column (#2187) Signed-off-by: Guan-Ming (Wesley) Chiu <[email protected]> --- src/ast/mod.rs | 34 +++++++++++++++++++--------------- src/ast/spans.rs | 4 +++- src/parser/mod.rs | 9 ++++++++- tests/sqlparser_postgres.rs | 25 +++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a5951969..010a8189 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3326,19 +3326,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, ...)]]` See <https://www.postgresql.org/docs/current/sql-analyze.html> +/// - 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<ObjectName>, /// Optional partition expressions to restrict the analysis. pub partitions: Option<Vec<Expr>>, - /// `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<Ident>, /// Whether to cache metadata before analyzing. pub cache_metadata: bool, @@ -3352,22 +3357,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 16a9a926..bdd430e7 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 14ddd2b5..585242a8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1193,13 +1193,20 @@ impl<'a> Parser<'a> { /// Parse `ANALYZE` statement. pub fn parse_analyze(&mut self) -> Result<Analyze, ParserError> { 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 5853be7e..cfb03737 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -8511,3 +8511,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:?}"), + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
