From 5fd28074c2bc61a84c5bf81ca86ad4f55ffc2ce7 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 19:07:16 +0100 Subject: [PATCH 1/4] feat(postgres): add statistics DDL AST and parser support --- src/ast/mod.rs | 182 +++++++++++++++++++++++++++++ src/ast/spans.rs | 2 + src/parser/mod.rs | 291 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 470 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1e430171e..bc455e175 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2459,6 +2459,8 @@ pub enum CommentObject { Schema, /// A sequence. Sequence, + /// Statistics object. + Statistics, /// A table. Table, /// A type. @@ -2483,6 +2485,7 @@ impl fmt::Display for CommentObject { CommentObject::Role => f.write_str("ROLE"), CommentObject::Schema => f.write_str("SCHEMA"), CommentObject::Sequence => f.write_str("SEQUENCE"), + CommentObject::Statistics => f.write_str("STATISTICS"), CommentObject::Table => f.write_str("TABLE"), CommentObject::Type => f.write_str("TYPE"), CommentObject::User => f.write_str("USER"), @@ -3707,6 +3710,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createopclass.html) CreateOperatorClass(CreateOperatorClass), /// ```sql + /// CREATE STATISTICS + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createstatistics.html) + CreateStatistics(CreateStatistics), + /// ```sql /// ALTER TABLE /// ``` AlterTable(AlterTable), @@ -3759,6 +3767,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alteropclass.html) AlterOperatorClass(AlterOperatorClass), /// ```sql + /// ALTER STATISTICS + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterstatistics.html) + AlterStatistics(AlterStatistics), + /// ```sql /// ALTER ROLE /// ``` AlterRole { @@ -5443,6 +5456,7 @@ impl fmt::Display for Statement { create_operator_family.fmt(f) } Statement::CreateOperatorClass(create_operator_class) => create_operator_class.fmt(f), + Statement::CreateStatistics(create_statistics) => create_statistics.fmt(f), Statement::AlterTable(alter_table) => write!(f, "{alter_table}"), Statement::AlterIndex { name, operation } => { write!(f, "ALTER INDEX {name} {operation}") @@ -5472,6 +5486,7 @@ impl fmt::Display for Statement { Statement::AlterOperatorClass(alter_operator_class) => { write!(f, "{alter_operator_class}") } + Statement::AlterStatistics(alter_statistics) => write!(f, "{alter_statistics}"), Statement::AlterRole { name, operation } => { write!(f, "ALTER ROLE {name} {operation}") } @@ -8246,6 +8261,8 @@ pub enum ObjectType { Role, /// A sequence. Sequence, + /// Statistics object. + Statistics, /// A stage. Stage, /// A type definition. @@ -8267,6 +8284,7 @@ impl fmt::Display for ObjectType { ObjectType::Database => "DATABASE", ObjectType::Role => "ROLE", ObjectType::Sequence => "SEQUENCE", + ObjectType::Statistics => "STATISTICS", ObjectType::Stage => "STAGE", ObjectType::Type => "TYPE", ObjectType::User => "USER", @@ -8697,6 +8715,158 @@ impl fmt::Display for SecretOption { } } +/// Statistics kinds supported by PostgreSQL `CREATE STATISTICS`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum StatisticsType { + /// `ndistinct` kind. + Ndistinct, + /// `dependencies` kind. + Dependencies, + /// `mcv` kind. + Mcv, + /// Any other statistics kind identifier accepted by PostgreSQL grammar. + Other(Ident), +} + +impl fmt::Display for StatisticsType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StatisticsType::Ndistinct => write!(f, "NDISTINCT"), + StatisticsType::Dependencies => write!(f, "DEPENDENCIES"), + StatisticsType::Mcv => write!(f, "MCV"), + StatisticsType::Other(ident) => write!(f, "{ident}"), + } + } +} + +/// A `CREATE STATISTICS` statement. +/// +/// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createstatistics.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateStatistics { + /// Whether `IF NOT EXISTS` was specified. + pub if_not_exists: bool, + /// Optional name of the statistics object. + pub name: Option, + /// Optional list of statistics kinds. + pub statistics_types: Vec, + /// Target expressions listed after `ON`. + pub columns: Vec, + /// Source relation list after `FROM`. + pub from: Vec, +} + +impl fmt::Display for CreateStatistics { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "CREATE STATISTICS")?; + if self.if_not_exists { + write!(f, " IF NOT EXISTS")?; + } + if let Some(name) = &self.name { + write!(f, " {name}")?; + } + if !self.statistics_types.is_empty() { + write!(f, " ({})", display_comma_separated(&self.statistics_types))?; + } + write!( + f, + " ON {} FROM {}", + display_comma_separated(&self.columns), + display_comma_separated(&self.from) + ) + } +} + +/// Target value for `ALTER STATISTICS ... SET STATISTICS`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterStatisticsTarget { + /// Numeric target value. + Value(i64), + /// `DEFAULT` target. + Default, +} + +impl fmt::Display for AlterStatisticsTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AlterStatisticsTarget::Value(value) => write!(f, "{value}"), + AlterStatisticsTarget::Default => write!(f, "DEFAULT"), + } + } +} + +/// Operation variants for `ALTER STATISTICS`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterStatisticsOperation { + /// `RENAME TO new_name`. + RenameTo { + /// New name of the statistics object. + new_name: ObjectName, + }, + /// `OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER }`. + OwnerTo(Owner), + /// `SET SCHEMA new_schema`. + SetSchema { + /// New schema for the statistics object. + schema_name: ObjectName, + }, + /// `SET STATISTICS { integer | DEFAULT }`. + SetStatistics { + /// New statistics target value. + target: AlterStatisticsTarget, + }, +} + +impl fmt::Display for AlterStatisticsOperation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AlterStatisticsOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + AlterStatisticsOperation::OwnerTo(owner) => write!(f, "OWNER TO {owner}"), + AlterStatisticsOperation::SetSchema { schema_name } => { + write!(f, "SET SCHEMA {schema_name}") + } + AlterStatisticsOperation::SetStatistics { target } => { + write!(f, "SET STATISTICS {target}") + } + } + } +} + +/// An `ALTER STATISTICS` statement. +/// +/// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-alterstatistics.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterStatistics { + /// Whether `IF EXISTS` was specified. + pub if_exists: bool, + /// Name of the statistics object. + pub name: ObjectName, + /// Operation to apply. + pub operation: AlterStatisticsOperation, +} + +impl fmt::Display for AlterStatistics { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ALTER STATISTICS {if_exists}{name} {operation}", + if_exists = if self.if_exists { "IF EXISTS " } else { "" }, + name = self.name, + operation = self.operation + ) + } +} + /// A `CREATE SERVER` statement. /// /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createserver.html) @@ -11912,6 +12082,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(c: CreateStatistics) -> Self { + Self::CreateStatistics(c) + } +} + impl From for Statement { fn from(a: AlterSchema) -> Self { Self::AlterSchema(a) @@ -11942,6 +12118,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(a: AlterStatistics) -> Self { + Self::AlterStatistics(a) + } +} + impl From for Statement { fn from(m: Merge) -> Self { Self::Merge(m) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0b95c3ed7..893f0443a 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -388,6 +388,7 @@ impl Spanned for Statement { create_operator_family.span() } Statement::CreateOperatorClass(create_operator_class) => create_operator_class.span(), + Statement::CreateStatistics(..) => Span::empty(), Statement::AlterTable(alter_table) => alter_table.span(), Statement::AlterIndex { name, operation } => name.span().union(&operation.span()), Statement::AlterView { @@ -406,6 +407,7 @@ impl Spanned for Statement { Statement::AlterOperator { .. } => Span::empty(), Statement::AlterOperatorFamily { .. } => Span::empty(), Statement::AlterOperatorClass { .. } => Span::empty(), + Statement::AlterStatistics(..) => Span::empty(), Statement::AlterRole { .. } => Span::empty(), Statement::AlterSession { .. } => Span::empty(), Statement::AttachDatabase { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bea566bbe..eca321ecb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -935,6 +935,10 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::SEQUENCE => { (CommentObject::Sequence, self.parse_object_name(false)?) } + Token::Word(w) if w.keyword == Keyword::STATISTICS => ( + CommentObject::Statistics, + self.parse_pg_object_name_strict()?, + ), Token::Word(w) if w.keyword == Keyword::TABLE => { (CommentObject::Table, self.parse_object_name(false)?) } @@ -5130,6 +5134,28 @@ impl<'a> Parser<'a> { self.parse_create_secret(or_replace, temporary, persistent) } else if self.parse_keyword(Keyword::USER) { self.parse_create_user(or_replace).map(Into::into) + } else if self.peek_keyword(Keyword::STATISTICS) { + let loc = self.peek_token_ref().span.start; + self.expect_keyword_is(Keyword::STATISTICS)?; + if or_replace { + return parser_err!("Cannot specify OR REPLACE in CREATE STATISTICS", loc); + } + if or_alter { + return parser_err!("Cannot specify OR ALTER in CREATE STATISTICS", loc); + } + if global.is_some() { + return parser_err!("Cannot specify LOCAL or GLOBAL in CREATE STATISTICS", loc); + } + if transient { + return parser_err!("Cannot specify TRANSIENT in CREATE STATISTICS", loc); + } + if temporary { + return parser_err!("Cannot specify TEMPORARY in CREATE STATISTICS", loc); + } + if persistent { + return parser_err!("Cannot specify PERSISTENT in CREATE STATISTICS", loc); + } + self.parse_create_statistics().map(Into::into) } else if or_replace { self.expected_ref( "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION after CREATE OR REPLACE", @@ -5173,6 +5199,167 @@ impl<'a> Parser<'a> { } } + /// Parse a `CREATE STATISTICS` statement. + pub fn parse_create_statistics(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = if self.peek_keyword(Keyword::ON) || self.peek_token_ref().token == Token::LParen + { + None + } else { + Some(self.parse_pg_object_name_strict()?) + }; + + if if_not_exists && name.is_none() { + return self.expected_ref("statistics name after IF NOT EXISTS", self.peek_token_ref()); + } + + let statistics_types = if self.consume_token(&Token::LParen) { + let types = self.parse_comma_separated(|p| p.parse_statistics_type())?; + self.expect_token(&Token::RParen)?; + types + } else { + vec![] + }; + + self.expect_keyword_is(Keyword::ON)?; + let columns = self.parse_comma_separated(Parser::parse_create_statistics_param)?; + self.expect_keyword_is(Keyword::FROM)?; + let from = self.parse_comma_separated(Parser::parse_table_and_joins)?; + self.validate_pg_statistics_from_list(&from)?; + + Ok(CreateStatistics { + if_not_exists, + name, + statistics_types, + columns, + from, + }) + } + + fn validate_pg_statistics_from_list(&self, from: &[TableWithJoins]) -> Result<(), ParserError> { + for table_with_joins in from { + self.validate_pg_statistics_table_with_joins(table_with_joins)?; + } + Ok(()) + } + + fn validate_pg_statistics_table_with_joins( + &self, + table_with_joins: &TableWithJoins, + ) -> Result<(), ParserError> { + self.validate_pg_statistics_table_factor(&table_with_joins.relation)?; + for join in &table_with_joins.joins { + self.validate_pg_statistics_table_factor(&join.relation)?; + } + Ok(()) + } + + fn validate_pg_statistics_table_factor( + &self, + relation: &TableFactor, + ) -> Result<(), ParserError> { + match relation { + TableFactor::Table { name, alias, .. } + | TableFactor::Function { name, alias, .. } + | TableFactor::SemanticView { name, alias, .. } => { + self.validate_no_single_quoted_object_name(name)?; + self.validate_pg_statistics_table_alias(alias.as_ref()) + } + TableFactor::NestedJoin { + table_with_joins, + alias, + } => { + self.validate_pg_statistics_table_with_joins(table_with_joins)?; + self.validate_pg_statistics_table_alias(alias.as_ref()) + } + TableFactor::Pivot { table, alias, .. } + | TableFactor::Unpivot { table, alias, .. } + | TableFactor::MatchRecognize { table, alias, .. } => { + self.validate_pg_statistics_table_factor(table)?; + self.validate_pg_statistics_table_alias(alias.as_ref()) + } + TableFactor::Derived { alias, .. } + | TableFactor::TableFunction { alias, .. } + | TableFactor::UNNEST { alias, .. } + | TableFactor::JsonTable { alias, .. } + | TableFactor::OpenJsonTable { alias, .. } + | TableFactor::XmlTable { alias, .. } => { + self.validate_pg_statistics_table_alias(alias.as_ref()) + } + } + } + + fn validate_pg_statistics_table_alias( + &self, + alias: Option<&TableAlias>, + ) -> Result<(), ParserError> { + if let Some(alias) = alias { + self.validate_pg_statistics_identifier(&alias.name)?; + for column in &alias.columns { + self.validate_pg_statistics_identifier(&column.name)?; + } + } + Ok(()) + } + + fn validate_pg_statistics_identifier(&self, ident: &Ident) -> Result<(), ParserError> { + if ident.quote_style == Some('\'') { + return self.expected( + "identifier", + TokenWithSpan::new(Token::SingleQuotedString(ident.value.clone()), ident.span), + ); + } + Ok(()) + } + + fn validate_no_single_quoted_object_name( + &self, + object_name: &ObjectName, + ) -> Result<(), ParserError> { + for part in &object_name.0 { + match part { + ObjectNamePart::Identifier(ident) => { + self.validate_pg_statistics_identifier(ident)? + } + ObjectNamePart::Function(func) => { + self.validate_pg_statistics_identifier(&func.name)? + } + } + } + Ok(()) + } + + fn parse_statistics_type(&mut self) -> Result { + let next_token = self.next_token(); + let ident = match next_token.token { + Token::Word(w) => w.into_ident(next_token.span), + _ => return self.expected("identifier", next_token), + }; + if ident.quote_style.is_none() { + if ident.value.eq_ignore_ascii_case("ndistinct") { + return Ok(StatisticsType::Ndistinct); + } else if ident.value.eq_ignore_ascii_case("dependencies") { + return Ok(StatisticsType::Dependencies); + } else if ident.value.eq_ignore_ascii_case("mcv") { + return Ok(StatisticsType::Mcv); + } + } + Ok(StatisticsType::Other(ident)) + } + + fn parse_create_statistics_param(&mut self) -> Result { + let expr = self.parse_expr()?; + match &expr { + Expr::Identifier(_) => Ok(expr), + Expr::Function(Function { over: None, .. }) => Ok(expr), + Expr::Nested(_) => Ok(expr), + _ => parser_err!( + "Expected column reference, windowless function, or parenthesized expression in CREATE STATISTICS ON list", + expr.span().start + ), + } + } + fn parse_create_user(&mut self, or_replace: bool) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let name = self.parse_identifier()?; @@ -7214,6 +7401,8 @@ impl<'a> Parser<'a> { ObjectType::Database } else if self.parse_keyword(Keyword::SEQUENCE) { ObjectType::Sequence + } else if self.parse_keyword(Keyword::STATISTICS) { + ObjectType::Statistics } else if self.parse_keyword(Keyword::STAGE) { ObjectType::Stage } else if self.parse_keyword(Keyword::TYPE) { @@ -7249,14 +7438,18 @@ impl<'a> Parser<'a> { }; } else { return self.expected_ref( - "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW or USER after DROP", + "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STATISTICS, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW or USER after DROP", self.peek_token_ref(), ); }; // Many dialects support the non-standard `IF EXISTS` clause and allow // specifying multiple objects to delete in a single statement let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let names = self.parse_comma_separated(|p| p.parse_object_name(false))?; + let names = if object_type == ObjectType::Statistics { + self.parse_comma_separated(|p| p.parse_pg_object_name_strict())? + } else { + self.parse_comma_separated(|p| p.parse_object_name(false))? + }; let loc = self.peek_token_ref().span.start; let cascade = self.parse_keyword(Keyword::CASCADE); @@ -7276,6 +7469,15 @@ impl<'a> Parser<'a> { } else { None }; + if object_type == ObjectType::Statistics && purge { + return parser_err!("Cannot specify PURGE in DROP STATISTICS", loc); + } + if object_type == ObjectType::Statistics && temporary { + return parser_err!("Cannot specify TEMPORARY in DROP STATISTICS", loc); + } + if object_type == ObjectType::Statistics && table.is_some() { + return parser_err!("Cannot specify ON in DROP STATISTICS", loc); + } Ok(Statement::Drop { object_type, if_exists, @@ -10440,6 +10642,7 @@ impl<'a> Parser<'a> { Keyword::SCHEMA, Keyword::USER, Keyword::OPERATOR, + Keyword::STATISTICS, ])?; match object_type { Keyword::SCHEMA => { @@ -10485,13 +10688,60 @@ impl<'a> Parser<'a> { Keyword::POLICY => self.parse_alter_policy().map(Into::into), Keyword::CONNECTOR => self.parse_alter_connector(), Keyword::USER => self.parse_alter_user().map(Into::into), + Keyword::STATISTICS => self.parse_alter_statistics().map(Into::into), // unreachable because expect_one_of_keywords used above unexpected_keyword => Err(ParserError::ParserError( - format!("Internal parser error: expected any of {{VIEW, TYPE, TABLE, INDEX, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR}}, got {unexpected_keyword:?}"), + format!("Internal parser error: expected any of {{VIEW, TYPE, TABLE, INDEX, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR, STATISTICS}}, got {unexpected_keyword:?}"), )), } } + /// Parse an `ALTER STATISTICS` statement. + pub fn parse_alter_statistics(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_pg_object_name_strict()?; + + let operation = if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterStatisticsOperation::RenameTo { + new_name: ObjectName::from(vec![self.parse_pg_identifier_strict()?]), + } + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + AlterStatisticsOperation::OwnerTo(self.parse_owner()?) + } else if self.parse_keywords(&[Keyword::SET, Keyword::SCHEMA]) { + AlterStatisticsOperation::SetSchema { + schema_name: ObjectName::from(vec![self.parse_pg_identifier_strict()?]), + } + } else if self.parse_keywords(&[Keyword::SET, Keyword::STATISTICS]) { + AlterStatisticsOperation::SetStatistics { + target: self.parse_alter_statistics_target()?, + } + } else { + return self.expected_ref( + "RENAME TO, OWNER TO, SET SCHEMA, or SET STATISTICS after ALTER STATISTICS", + self.peek_token_ref(), + ); + }; + if if_exists && !matches!(operation, AlterStatisticsOperation::SetStatistics { .. }) { + return parser_err!( + "IF EXISTS is only supported with ALTER STATISTICS ... SET STATISTICS", + self.peek_token_ref().span.start + ); + } + Ok(AlterStatistics { + if_exists, + name, + operation, + }) + } + + fn parse_alter_statistics_target(&mut self) -> Result { + if self.parse_keyword(Keyword::DEFAULT) { + Ok(AlterStatisticsTarget::Default) + } else { + Ok(AlterStatisticsTarget::Value(self.parse_signed_integer()?)) + } + } + /// Parse a [Statement::AlterTable] pub fn parse_alter_table(&mut self, iceberg: bool) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); @@ -12388,7 +12638,9 @@ impl<'a> Parser<'a> { Ok(Some(w.into_ident(next_token.span))) } // For backwards-compatibility, we accept quoted strings as aliases regardless of the context. - Token::SingleQuotedString(s) => Ok(Some(Ident::with_quote('\'', s))), + Token::SingleQuotedString(s) => { + Ok(Some(Ident::with_quote_and_span('\'', next_token.span, s))) + } Token::DoubleQuotedString(s) => Ok(Some(Ident::with_quote('\"', s))), _ => { if after_as { @@ -12770,12 +13022,41 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { Token::Word(w) => Ok(w.into_ident(next_token.span)), - Token::SingleQuotedString(s) => Ok(Ident::with_quote('\'', s)), + Token::SingleQuotedString(s) => { + Ok(Ident::with_quote_and_span('\'', next_token.span, s)) + } Token::DoubleQuotedString(s) => Ok(Ident::with_quote('\"', s)), _ => self.expected("identifier", next_token), } } + /// Parse a PostgreSQL identifier token for contexts where single-quoted + /// string literals must not be accepted as identifiers. + fn parse_pg_identifier_strict(&mut self) -> Result { + let next_token = self.next_token(); + match next_token.token { + Token::Word(w) => Ok(w.into_ident(next_token.span)), + Token::DoubleQuotedString(s) => { + Ok(Ident::with_quote_and_span('\"', next_token.span, s)) + } + _ => self.expected("identifier", next_token), + } + } + + /// Parse a PostgreSQL object name for contexts that disallow single-quoted + /// identifier parts. + fn parse_pg_object_name_strict(&mut self) -> Result { + let mut parts = vec![ObjectNamePart::Identifier( + self.parse_pg_identifier_strict()?, + )]; + while self.consume_token(&Token::Period) { + parts.push(ObjectNamePart::Identifier( + self.parse_pg_identifier_strict()?, + )); + } + Ok(ObjectName(parts)) + } + /// On BigQuery, hyphens are permitted in unquoted identifiers inside of a FROM or /// TABLE clause. /// From d7fefda652f046cf83ed789b96344697bc5aca91 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 19:07:20 +0100 Subject: [PATCH 2/4] test(postgres): cover statistics syntax, strictness, and errors --- tests/sqlparser_common.rs | 1 + tests/sqlparser_postgres.rs | 420 +++++++++++++++++++++++++++++++++++- 2 files changed, 420 insertions(+), 1 deletion(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 982bf1088..0099a376b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15218,6 +15218,7 @@ fn parse_comments() { ("ROLE", CommentObject::Role), ("SCHEMA", CommentObject::Schema), ("SEQUENCE", CommentObject::Sequence), + ("STATISTICS", CommentObject::Statistics), ("TABLE", CommentObject::Table), ("TYPE", CommentObject::Type), ("USER", CommentObject::User), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7c19f51e5..9790441fb 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -25,7 +25,7 @@ mod test_utils; use helpers::attached_token::AttachedToken; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, PostgreSqlDialect}; -use sqlparser::parser::ParserError; +use sqlparser::parser::{Parser, ParserError}; use sqlparser::tokenizer::Span; use test_utils::*; @@ -8602,3 +8602,421 @@ fn parse_pg_analyze() { _ => panic!("Expected Analyze, got: {stmt:?}"), } } + +#[test] +fn parse_create_statistics() { + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON a, b FROM ext_stats_test", + "CREATE STATISTICS tst ON a, b FROM ext_stats_test", + ); + + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS ON (date_trunc('day', d)) FROM ab1", + "CREATE STATISTICS ON (date_trunc('day', d)) FROM ab1", + ); + + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON date_trunc('day', d) FROM ab1", + "CREATE STATISTICS tst ON date_trunc('day', d) FROM ab1", + ); + + let stmt = pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst (ndistinct, dependencies, mcv) ON (z + 1), z FROM ext_stats_test1", + "CREATE STATISTICS tst (NDISTINCT, DEPENDENCIES, MCV) ON (z + 1), z FROM ext_stats_test1", + ); + let Statement::CreateStatistics(create_statistics) = stmt else { + panic!("Expected CREATE STATISTICS statement"); + }; + assert_eq!( + create_statistics.statistics_types, + vec![ + StatisticsType::Ndistinct, + StatisticsType::Dependencies, + StatisticsType::Mcv + ] + ); + assert_eq!( + create_statistics.name, + Some(ObjectName::from(vec![Ident::new("tst")])) + ); + assert_eq!(create_statistics.columns.len(), 2); + assert_eq!(create_statistics.from.len(), 1); + assert_eq!(create_statistics.from[0].to_string(), "ext_stats_test1"); + + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS IF NOT EXISTS ab1_a_b_stats ON a, b FROM ab1", + "CREATE STATISTICS IF NOT EXISTS ab1_a_b_stats ON a, b FROM ab1", + ); + + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON date_trunc('day', d), (case a when 1 then true else false end), b FROM ab1", + "CREATE STATISTICS tst ON date_trunc('day', d), (CASE a WHEN 1 THEN true ELSE false END), b FROM ab1", + ); + + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON date_trunc('day', d), y FROM ext_stats_test", + "CREATE STATISTICS tst ON date_trunc('day', d), y FROM ext_stats_test", + ); + + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON (rank() OVER ()), a FROM ab1", + "CREATE STATISTICS tst ON (rank() OVER ()), a FROM ab1", + ); + + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS (ndistinct) ON a, b FROM ab1", + "CREATE STATISTICS (NDISTINCT) ON a, b FROM ab1", + ); + + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst (unrecognized, \"ndistinct\") ON x FROM ext_stats_test", + "CREATE STATISTICS tst (unrecognized, \"ndistinct\") ON x FROM ext_stats_test", + ); + let stmt = pg_and_generic().verified_stmt( + "CREATE STATISTICS tst (unrecognized, \"ndistinct\") ON x FROM ext_stats_test", + ); + let Statement::CreateStatistics(create_statistics) = stmt else { + panic!("Expected CREATE STATISTICS statement"); + }; + assert_eq!( + create_statistics.statistics_types, + vec![ + StatisticsType::Other(Ident::new("unrecognized")), + StatisticsType::Other(Ident::with_quote('"', "ndistinct")) + ] + ); + + // Relation existence and duplicate statistic targets are validated by PostgreSQL's + // semantic analysis, so these should still parse. + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON a, b FROM nonexistent", + "CREATE STATISTICS tst ON a, b FROM nonexistent", + ); + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON x, x, y FROM ext_stats_test", + "CREATE STATISTICS tst ON x, x, y FROM ext_stats_test", + ); + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON (x || 'x'), (x || 'x'), (y + 1) FROM ext_stats_test", + "CREATE STATISTICS tst ON (x || 'x'), (x || 'x'), (y + 1) FROM ext_stats_test", + ); + + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON a FROM (VALUES (x)) AS foo", + "CREATE STATISTICS tst ON a FROM (VALUES (x)) AS foo", + ); + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON a FROM foo NATURAL JOIN bar", + "CREATE STATISTICS tst ON a FROM foo NATURAL JOIN bar", + ); + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON a FROM (SELECT * FROM ext_stats_test) AS foo", + "CREATE STATISTICS tst ON a FROM (SELECT * FROM ext_stats_test) AS foo", + ); + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON a FROM ext_stats_test s TABLESAMPLE system (x)", + "CREATE STATISTICS tst ON a FROM ext_stats_test s TABLESAMPLE SYSTEM (x)", + ); + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON a FROM XMLTABLE('foo' PASSING 'bar' COLUMNS a text)", + "CREATE STATISTICS tst ON a FROM XMLTABLE('foo' PASSING 'bar' COLUMNS a TEXT)", + ); + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS tst ON a FROM JSON_TABLE('[1,2]', '$[*]' COLUMNS(item INT PATH '$')) AS jt", + "CREATE STATISTICS tst ON a FROM JSON_TABLE('[1,2]', '$[*]' COLUMNS(item INT PATH '$')) AS jt", + ); + pg_and_generic().one_statement_parses_to( + "CREATE STATISTICS alt_stat2 ON a FROM tftest(1)", + "CREATE STATISTICS alt_stat2 ON a FROM tftest(1)", + ); +} + +#[test] +fn parse_alter_statistics() { + assert_eq!( + pg_and_generic() + .verified_stmt("ALTER STATISTICS ab1_a_b_stats RENAME TO ab1_a_b_stats_new"), + Statement::AlterStatistics(AlterStatistics { + if_exists: false, + name: ObjectName::from(vec![Ident::new("ab1_a_b_stats")]), + operation: AlterStatisticsOperation::RenameTo { + new_name: ObjectName::from(vec![Ident::new("ab1_a_b_stats_new")]), + }, + }) + ); + + assert_eq!( + pg_and_generic().verified_stmt("ALTER STATISTICS IF EXISTS ab1_a_b_stats SET STATISTICS 0"), + Statement::AlterStatistics(AlterStatistics { + if_exists: true, + name: ObjectName::from(vec![Ident::new("ab1_a_b_stats")]), + operation: AlterStatisticsOperation::SetStatistics { + target: AlterStatisticsTarget::Value(0), + }, + }) + ); + + assert_eq!( + pg_and_generic().verified_stmt("ALTER STATISTICS ab1_a_b_stats SET STATISTICS DEFAULT"), + Statement::AlterStatistics(AlterStatistics { + if_exists: false, + name: ObjectName::from(vec![Ident::new("ab1_a_b_stats")]), + operation: AlterStatisticsOperation::SetStatistics { + target: AlterStatisticsTarget::Default, + }, + }) + ); + + assert_eq!( + pg_and_generic().verified_stmt("ALTER STATISTICS ab1_a_b_stats SET STATISTICS -1"), + Statement::AlterStatistics(AlterStatistics { + if_exists: false, + name: ObjectName::from(vec![Ident::new("ab1_a_b_stats")]), + operation: AlterStatisticsOperation::SetStatistics { + target: AlterStatisticsTarget::Value(-1), + }, + }) + ); + + assert_eq!( + pg_and_generic().verified_stmt("ALTER STATISTICS ab1_a_b_stats OWNER TO CURRENT_USER"), + Statement::AlterStatistics(AlterStatistics { + if_exists: false, + name: ObjectName::from(vec![Ident::new("ab1_a_b_stats")]), + operation: AlterStatisticsOperation::OwnerTo(Owner::CurrentUser), + }) + ); + + assert_eq!( + pg_and_generic().verified_stmt("ALTER STATISTICS ab1_a_b_stats SET SCHEMA new_schema"), + Statement::AlterStatistics(AlterStatistics { + if_exists: false, + name: ObjectName::from(vec![Ident::new("ab1_a_b_stats")]), + operation: AlterStatisticsOperation::SetSchema { + schema_name: ObjectName::from(vec![Ident::new("new_schema")]), + }, + }) + ); +} + +#[test] +fn parse_drop_statistics() { + assert_eq!( + pg_and_generic().verified_stmt("DROP STATISTICS ab1_a_b_stats"), + Statement::Drop { + object_type: ObjectType::Statistics, + if_exists: false, + names: vec![ObjectName::from(vec![Ident::new("ab1_a_b_stats")])], + cascade: false, + restrict: false, + purge: false, + temporary: false, + table: None, + } + ); + + pg_and_generic().one_statement_parses_to( + "DROP STATISTICS IF EXISTS stats_ext_temp, pg_temp.stats_ext_temp", + "DROP STATISTICS IF EXISTS stats_ext_temp, pg_temp.stats_ext_temp", + ); +} + +#[test] +fn parse_comment_on_statistics() { + assert_eq!( + pg_and_generic().verified_stmt("COMMENT ON STATISTICS ab1_a_b_stats IS 'new comment'"), + Statement::Comment { + object_type: CommentObject::Statistics, + object_name: ObjectName::from(vec![Ident::new("ab1_a_b_stats")]), + comment: Some("new comment".to_string()), + if_exists: false, + } + ); + + pg_and_generic().one_statement_parses_to( + "COMMENT ON STATISTICS ab1_a_b_stats IS NULL", + "COMMENT ON STATISTICS ab1_a_b_stats IS NULL", + ); +} + +#[test] +fn parse_statistics_syntax_errors() { + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS IF NOT EXISTS ON (a + 1) FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS tst ON FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS tst ON rank() OVER () FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS tst ON rank() OVER (), a FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS tst ON x + 1, y FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS tst ON t.a, b FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS tst ON rank() OVER () + 1, a FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS tst ('foo') ON x FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS tst ('ndistinct') ON x FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS 'tst' ON x FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS tst ON a FROM 'ext_stats_test'") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE TEMPORARY STATISTICS tst ON a FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE TRANSIENT STATISTICS tst ON a FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE LOCAL STATISTICS tst ON a FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE GLOBAL STATISTICS tst ON a FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE OR REPLACE STATISTICS tst ON a FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE OR ALTER STATISTICS tst ON a FROM ext_stats_test") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS tst ON a FROM ext_stats_test 's'") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("CREATE STATISTICS tst ON a FROM (SELECT 1) 's'") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements( + "CREATE STATISTICS tst ON a FROM JSON_TABLE('[1,2]', '$[*]' COLUMNS(item INT PATH '$')) 'jt'" + ) + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("ALTER STATISTICS 'ab1_a_b_stats' SET STATISTICS 0") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("DROP STATISTICS 'ab1_a_b_stats'") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("COMMENT ON STATISTICS 'ab1_a_b_stats' IS 'new comment'") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("ALTER STATISTICS ab1_a_b_stats SET STATISTICS") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements( + "ALTER STATISTICS IF EXISTS ab1_a_b_stats RENAME TO ab1_a_b_stats_new" + ) + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("ALTER STATISTICS IF EXISTS ab1_a_b_stats OWNER TO CURRENT_USER") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("ALTER STATISTICS IF EXISTS ab1_a_b_stats SET SCHEMA new_schema") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("ALTER STATISTICS ab1_a_b_stats") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("ALTER STATISTICS ab1_a_b_stats RENAME TO new_schema.ab1_a_b_stats") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("ALTER STATISTICS ab1_a_b_stats SET SCHEMA new_schema.other") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("DROP STATISTICS ab1_a_b_stats ON ab1") + .is_err()); + + assert!(pg_and_generic() + .parse_sql_statements("DROP STATISTICS ab1_a_b_stats PURGE") + .is_err()); + + assert!(pg() + .parse_sql_statements("DROP TEMPORARY STATISTICS ab1_a_b_stats") + .is_err()); + + assert!(TestedDialects::new(vec![Box::new(GenericDialect {})]) + .parse_sql_statements("DROP TEMPORARY STATISTICS ab1_a_b_stats") + .is_err()); +} + +#[test] +fn parse_statistics_identifier_error_style() { + let err = Parser::new(&PostgreSqlDialect {}) + .try_with_sql("CREATE STATISTICS tst ON a FROM ext_stats_test 's'") + .unwrap() + .parse_statements() + .unwrap_err() + .to_string(); + assert!( + err.contains("Expected: identifier"), + "Expected identifier-style parser error, got: {err}" + ); + assert!( + err.contains("Line: 1"), + "Expected location info in parser error, got: {err}" + ); +} + +#[test] +fn parse_statistics_modifier_error_style() { + let err = pg_and_generic() + .parse_sql_statements("CREATE OR REPLACE STATISTICS tst ON a FROM ext_stats_test") + .unwrap_err() + .to_string(); + assert!( + err.contains("Cannot specify OR REPLACE in CREATE STATISTICS"), + "Expected CREATE STATISTICS modifier error, got: {err}" + ); + + let err = pg_and_generic() + .parse_sql_statements("CREATE OR ALTER STATISTICS tst ON a FROM ext_stats_test") + .unwrap_err() + .to_string(); + assert!( + err.contains("Cannot specify OR ALTER in CREATE STATISTICS"), + "Expected CREATE STATISTICS modifier error, got: {err}" + ); +} From 5d51c0ffc5b2df2615a6d79567dd7f6fda8b7783 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 22:26:30 +0100 Subject: [PATCH 3/4] docs(parser): clarify why CREATE STATISTICS uses post-parse FROM validation --- src/parser/mod.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index eca321ecb..b46c9b4b1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5225,6 +5225,10 @@ impl<'a> Parser<'a> { let columns = self.parse_comma_separated(Parser::parse_create_statistics_param)?; self.expect_keyword_is(Keyword::FROM)?; let from = self.parse_comma_separated(Parser::parse_table_and_joins)?; + // `parse_table_and_joins` is intentionally permissive and supports legacy + // forms (including single-quoted aliases) for broad compatibility. + // PostgreSQL `CREATE STATISTICS` is stricter, so we enforce those rules + // here without tightening FROM parsing globally. self.validate_pg_statistics_from_list(&from)?; Ok(CreateStatistics { @@ -5236,6 +5240,11 @@ impl<'a> Parser<'a> { }) } + /// Validate `FROM from_list` after generic table-factor parsing. + /// + /// This pass is required because `CREATE STATISTICS` uses PostgreSQL object + /// name semantics for relation and alias identifiers, while the shared table + /// parser accepts additional legacy forms in other contexts. fn validate_pg_statistics_from_list(&self, from: &[TableWithJoins]) -> Result<(), ParserError> { for table_with_joins in from { self.validate_pg_statistics_table_with_joins(table_with_joins)?; @@ -5258,6 +5267,8 @@ impl<'a> Parser<'a> { &self, relation: &TableFactor, ) -> Result<(), ParserError> { + // Recurse through wrappers so strict identifier checks are applied to + // every relation/alias position reachable from CREATE STATISTICS FROM. match relation { TableFactor::Table { name, alias, .. } | TableFactor::Function { name, alias, .. } @@ -5303,6 +5314,7 @@ impl<'a> Parser<'a> { } fn validate_pg_statistics_identifier(&self, ident: &Ident) -> Result<(), ParserError> { + // Single-quoted text is a string literal in PostgreSQL, not an identifier. if ident.quote_style == Some('\'') { return self.expected( "identifier", From 03fa10eedfed4ae6eef36f3eef8f0d627c2ad337 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 22:34:56 +0100 Subject: [PATCH 4/4] refactor(postgres): rename validation functions for PostgreSQL statistics identifiers --- src/parser/mod.rs | 52 +++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b46c9b4b1..fe50d774a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5229,7 +5229,7 @@ impl<'a> Parser<'a> { // forms (including single-quoted aliases) for broad compatibility. // PostgreSQL `CREATE STATISTICS` is stricter, so we enforce those rules // here without tightening FROM parsing globally. - self.validate_pg_statistics_from_list(&from)?; + self.enforce_pg_statistics_identifier_rules_in_from(&from)?; Ok(CreateStatistics { if_not_exists, @@ -5240,30 +5240,38 @@ impl<'a> Parser<'a> { }) } - /// Validate `FROM from_list` after generic table-factor parsing. + /// Enforce PostgreSQL identifier rules for `CREATE STATISTICS ... FROM`. /// /// This pass is required because `CREATE STATISTICS` uses PostgreSQL object /// name semantics for relation and alias identifiers, while the shared table /// parser accepts additional legacy forms in other contexts. - fn validate_pg_statistics_from_list(&self, from: &[TableWithJoins]) -> Result<(), ParserError> { + /// + /// The parser already uses this post-parse pattern for context-specific + /// constraints in other places (for example, `parse_all_or_distinct`, + /// `parse_duplicate_treatment`, DROP option-combination checks, and + /// `ALTER STATISTICS IF EXISTS` operation checks). + fn enforce_pg_statistics_identifier_rules_in_from( + &self, + from: &[TableWithJoins], + ) -> Result<(), ParserError> { for table_with_joins in from { - self.validate_pg_statistics_table_with_joins(table_with_joins)?; + self.enforce_pg_statistics_identifier_rules_in_table_with_joins(table_with_joins)?; } Ok(()) } - fn validate_pg_statistics_table_with_joins( + fn enforce_pg_statistics_identifier_rules_in_table_with_joins( &self, table_with_joins: &TableWithJoins, ) -> Result<(), ParserError> { - self.validate_pg_statistics_table_factor(&table_with_joins.relation)?; + self.enforce_pg_statistics_identifier_rules_in_table_factor(&table_with_joins.relation)?; for join in &table_with_joins.joins { - self.validate_pg_statistics_table_factor(&join.relation)?; + self.enforce_pg_statistics_identifier_rules_in_table_factor(&join.relation)?; } Ok(()) } - fn validate_pg_statistics_table_factor( + fn enforce_pg_statistics_identifier_rules_in_table_factor( &self, relation: &TableFactor, ) -> Result<(), ParserError> { @@ -5273,21 +5281,21 @@ impl<'a> Parser<'a> { TableFactor::Table { name, alias, .. } | TableFactor::Function { name, alias, .. } | TableFactor::SemanticView { name, alias, .. } => { - self.validate_no_single_quoted_object_name(name)?; - self.validate_pg_statistics_table_alias(alias.as_ref()) + self.enforce_pg_object_name_identifier_rules(name)?; + self.enforce_pg_identifier_rules_in_table_alias(alias.as_ref()) } TableFactor::NestedJoin { table_with_joins, alias, } => { - self.validate_pg_statistics_table_with_joins(table_with_joins)?; - self.validate_pg_statistics_table_alias(alias.as_ref()) + self.enforce_pg_statistics_identifier_rules_in_table_with_joins(table_with_joins)?; + self.enforce_pg_identifier_rules_in_table_alias(alias.as_ref()) } TableFactor::Pivot { table, alias, .. } | TableFactor::Unpivot { table, alias, .. } | TableFactor::MatchRecognize { table, alias, .. } => { - self.validate_pg_statistics_table_factor(table)?; - self.validate_pg_statistics_table_alias(alias.as_ref()) + self.enforce_pg_statistics_identifier_rules_in_table_factor(table)?; + self.enforce_pg_identifier_rules_in_table_alias(alias.as_ref()) } TableFactor::Derived { alias, .. } | TableFactor::TableFunction { alias, .. } @@ -5295,25 +5303,25 @@ impl<'a> Parser<'a> { | TableFactor::JsonTable { alias, .. } | TableFactor::OpenJsonTable { alias, .. } | TableFactor::XmlTable { alias, .. } => { - self.validate_pg_statistics_table_alias(alias.as_ref()) + self.enforce_pg_identifier_rules_in_table_alias(alias.as_ref()) } } } - fn validate_pg_statistics_table_alias( + fn enforce_pg_identifier_rules_in_table_alias( &self, alias: Option<&TableAlias>, ) -> Result<(), ParserError> { if let Some(alias) = alias { - self.validate_pg_statistics_identifier(&alias.name)?; + self.enforce_pg_identifier_token_rules(&alias.name)?; for column in &alias.columns { - self.validate_pg_statistics_identifier(&column.name)?; + self.enforce_pg_identifier_token_rules(&column.name)?; } } Ok(()) } - fn validate_pg_statistics_identifier(&self, ident: &Ident) -> Result<(), ParserError> { + fn enforce_pg_identifier_token_rules(&self, ident: &Ident) -> Result<(), ParserError> { // Single-quoted text is a string literal in PostgreSQL, not an identifier. if ident.quote_style == Some('\'') { return self.expected( @@ -5324,17 +5332,17 @@ impl<'a> Parser<'a> { Ok(()) } - fn validate_no_single_quoted_object_name( + fn enforce_pg_object_name_identifier_rules( &self, object_name: &ObjectName, ) -> Result<(), ParserError> { for part in &object_name.0 { match part { ObjectNamePart::Identifier(ident) => { - self.validate_pg_statistics_identifier(ident)? + self.enforce_pg_identifier_token_rules(ident)? } ObjectNamePart::Function(func) => { - self.validate_pg_statistics_identifier(&func.name)? + self.enforce_pg_identifier_token_rules(&func.name)? } } }