From cf25749ae9446e5e8b91cc811aac760a3a84acd5 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Wed, 6 Nov 2024 11:00:03 +0100 Subject: [PATCH 1/6] Add support for Snowflake SHOW DATABASES/SCHEMAS/TABLES/VIEWS/COLUMNS statements --- src/ast/mod.rs | 332 +++++++++++++++++++++++++++++------ src/dialect/mod.rs | 6 + src/dialect/snowflake.rs | 6 + src/keywords.rs | 3 + src/parser/mod.rs | 274 ++++++++++++++++++++++++----- tests/sqlparser_common.rs | 50 ++++-- tests/sqlparser_mysql.rs | 111 +++++++++--- tests/sqlparser_snowflake.rs | 65 +++++++ 8 files changed, 717 insertions(+), 130 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 81bddcd17..80faafc7d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2773,41 +2773,65 @@ pub enum Statement { /// ```sql /// SHOW COLUMNS /// ``` - /// - /// Note: this is a MySQL-specific statement. ShowColumns { extended: bool, full: bool, - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, + show_in: Option, filter: Option, + filter_position: ShowStatementFilterPosition, }, /// ```sql - /// SHOW DATABASES [LIKE 'pattern'] + /// SHOW DATABASES /// ``` - ShowDatabases { filter: Option }, + ShowDatabases { + terse: bool, + history: bool, + filter: Option, + show_in: Option, + starts_with: Option, + limit: Option, + from: Option, + }, /// ```sql - /// SHOW SCHEMAS [LIKE 'pattern'] + /// SHOW SCHEMAS /// ``` - ShowSchemas { filter: Option }, + ShowSchemas { + terse: bool, + history: bool, + filter: Option, + show_in: Option, + starts_with: Option, + limit: Option, + from: Option, + }, /// ```sql /// SHOW TABLES /// ``` ShowTables { + terse: bool, + history: bool, extended: bool, full: bool, - clause: Option, - db_name: Option, + external: bool, filter: Option, + show_in: Option, + starts_with: Option, + limit: Option, + from: Option, + filter_position: ShowStatementFilterPosition, }, /// ```sql /// SHOW VIEWS /// ``` ShowViews { + terse: bool, materialized: bool, - clause: Option, - db_name: Option, filter: Option, + show_in: Option, + starts_with: Option, + limit: Option, + from: Option, + filter_position: ShowStatementFilterPosition, }, /// ```sql /// SHOW COLLATION @@ -4387,79 +4411,226 @@ impl fmt::Display for Statement { Statement::ShowColumns { extended, full, - table_name, + show_in, filter, + filter_position, } => { write!( f, - "SHOW {extended}{full}COLUMNS FROM {table_name}", + "SHOW {extended}{full}COLUMNS", extended = if *extended { "EXTENDED " } else { "" }, full = if *full { "FULL " } else { "" }, - table_name = table_name, )?; - if let Some(filter) = filter { - write!(f, " {filter}")?; + if filter_position == &ShowStatementFilterPosition::InTheMiddle { + if let Some(filter) = filter { + write!(f, " {filter}")?; + } + if let Some(show_in) = show_in { + write!(f, " {show_in}")?; + } + } + if filter_position == &ShowStatementFilterPosition::AtTheEnd { + if let Some(show_in) = show_in { + write!(f, " {show_in}")?; + } + if let Some(filter) = filter { + write!(f, " {filter}")?; + } } Ok(()) } - Statement::ShowDatabases { filter } => { - write!(f, "SHOW DATABASES")?; - if let Some(filter) = filter { - write!(f, " {filter}")?; - } + Statement::ShowDatabases { + terse, + history, + filter, + show_in, + starts_with, + limit, + from, + } => { + write!( + f, + "SHOW {terse}DATABASES", + terse = if *terse { "TERSE " } else { "" } + )?; + write!( + f, + "{history}{filter}{show_in}{starts_with}{limit}{from}", + history = if *history { " HISTORY" } else { "" }, + filter = match filter.as_ref() { + Some(l) => format!(" {l}"), + None => String::new(), + }, + show_in = match show_in { + Some(i) => format!(" {i}"), + None => String::new(), + }, + starts_with = match starts_with.as_ref() { + Some(s) => format!(" STARTS WITH {s}"), + None => String::new(), + }, + limit = match limit.as_ref() { + Some(l) => format!(" LIMIT {l}"), + None => String::new(), + }, + from = match from.as_ref() { + Some(f) => format!(" FROM {f}"), + None => String::new(), + } + )?; Ok(()) } - Statement::ShowSchemas { filter } => { - write!(f, "SHOW SCHEMAS")?; - if let Some(filter) = filter { - write!(f, " {filter}")?; - } + Statement::ShowSchemas { + terse, + history, + filter, + show_in, + starts_with, + limit, + from, + } => { + write!( + f, + "SHOW {terse}SCHEMAS", + terse = if *terse { "TERSE " } else { "" } + )?; + write!( + f, + "{history}{filter}{show_in}{starts_with}{limit}{from}", + history = if *history { " HISTORY" } else { "" }, + filter = match filter.as_ref() { + Some(l) => format!(" {l}"), + None => String::new(), + }, + show_in = match show_in { + Some(i) => format!(" {i}"), + None => String::new(), + }, + starts_with = match starts_with.as_ref() { + Some(s) => format!(" STARTS WITH {s}"), + None => String::new(), + }, + limit = match limit.as_ref() { + Some(l) => format!(" LIMIT {l}"), + None => String::new(), + }, + from = match from.as_ref() { + Some(f) => format!(" FROM {f}"), + None => String::new(), + } + )?; Ok(()) } Statement::ShowTables { + terse, + history, extended, full, - clause: show_clause, - db_name, + external, filter, + show_in, + starts_with, + limit, + from, + filter_position, } => { write!( f, - "SHOW {extended}{full}TABLES", + "SHOW {terse}{extended}{full}{external}TABLES", + terse = if *terse { "TERSE " } else { "" }, extended = if *extended { "EXTENDED " } else { "" }, full = if *full { "FULL " } else { "" }, + external = if *external { "EXTERNAL " } else { "" }, )?; - if let Some(show_clause) = show_clause { - write!(f, " {show_clause}")?; - } - if let Some(db_name) = db_name { - write!(f, " {db_name}")?; - } - if let Some(filter) = filter { - write!(f, " {filter}")?; - } - Ok(()) + write!( + f, + "{history}{like_in_the_middle}{show_in}{starts_with}{limit}{from}{like_at_the_end}", + history = if *history { " HISTORY" } else { "" }, + like_in_the_middle = if *filter_position + == ShowStatementFilterPosition::InTheMiddle + && filter.is_some() + { + format!(" {}", filter.as_ref().unwrap()) + } else { + String::new() + }, + show_in = match show_in { + Some(i) => format!(" {i}"), + None => String::new(), + }, + starts_with = match starts_with.as_ref() { + Some(s) => format!(" STARTS WITH {s}"), + None => String::new(), + }, + limit = match limit.as_ref() { + Some(l) => format!(" LIMIT {l}"), + None => String::new(), + }, + from = match from.as_ref() { + Some(f) => format!(" FROM {f}"), + None => String::new(), + }, + like_at_the_end = if *filter_position + == ShowStatementFilterPosition::AtTheEnd + && filter.is_some() + { + format!(" {}", filter.as_ref().unwrap()) + } else { + String::new() + }, + ) } Statement::ShowViews { + terse, materialized, - clause: show_clause, - db_name, filter, + show_in, + starts_with, + limit, + from, + filter_position, } => { write!( f, - "SHOW {}VIEWS", - if *materialized { "MATERIALIZED " } else { "" } + "SHOW {terse}{materialized}VIEWS", + terse = if *terse { "TERSE " } else { "" }, + materialized = if *materialized { "MATERIALIZED " } else { "" } + )?; + write!( + f, + "{like_in_the_middle}{show_in}{starts_with}{limit}{from}{like_at_the_end}", + like_in_the_middle = if *filter_position + == ShowStatementFilterPosition::InTheMiddle + && filter.is_some() + { + format!(" {}", filter.as_ref().unwrap()) + } else { + String::new() + }, + show_in = match show_in { + Some(i) => format!(" {i}"), + None => String::new(), + }, + starts_with = match starts_with.as_ref() { + Some(s) => format!(" STARTS WITH {s}"), + None => String::new(), + }, + limit = match limit.as_ref() { + Some(l) => format!(" LIMIT {l}"), + None => String::new(), + }, + from = match from.as_ref() { + Some(f) => format!(" FROM {f}"), + None => String::new(), + }, + like_at_the_end = if *filter_position == ShowStatementFilterPosition::AtTheEnd + && filter.is_some() + { + format!(" {}", filter.as_ref().unwrap()) + } else { + String::new() + } )?; - if let Some(show_clause) = show_clause { - write!(f, " {show_clause}")?; - } - if let Some(db_name) = db_name { - write!(f, " {db_name}")?; - } - if let Some(filter) = filter { - write!(f, " {filter}")?; - } Ok(()) } Statement::ShowFunctions { filter } => { @@ -6172,14 +6343,14 @@ impl fmt::Display for ShowStatementFilter { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum ShowClause { +pub enum ShowStatementInClause { IN, FROM, } -impl fmt::Display for ShowClause { +impl fmt::Display for ShowStatementInClause { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use ShowClause::*; + use ShowStatementInClause::*; match self { FROM => write!(f, "FROM"), IN => write!(f, "IN"), @@ -7357,6 +7528,55 @@ impl Display for UtilityOption { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +pub enum ShowStatementFilterPosition { + InTheMiddle, // Snowflake like + AtTheEnd, // MySQL like +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ShowStatementInParentType { + Account, + Database, + Schema, + Table, + View, +} + +impl fmt::Display for ShowStatementInParentType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ShowStatementInParentType::Account => write!(f, "ACCOUNT"), + ShowStatementInParentType::Database => write!(f, "DATABASE"), + ShowStatementInParentType::Schema => write!(f, "SCHEMA"), + ShowStatementInParentType::Table => write!(f, "TABLE"), + ShowStatementInParentType::View => write!(f, "VIEW"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ShowStatementIn { + pub clause: ShowStatementInClause, + pub parent_type: Option, + pub parent_name: Option, +} + +impl fmt::Display for ShowStatementIn { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.clause)?; + if let Some(parent_type) = &self.parent_type { + write!(f, " {}", parent_type)?; + } + if let Some(parent_name) = &self.parent_name { + write!(f, " {}", parent_name)?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c8c11bc95..3a1ff745d 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -622,6 +622,12 @@ pub trait Dialect: Debug + Any { fn supports_boolean_literals(&self) -> bool { true } + + /// Returns true if this dialect support the `LIKE 'pattern'` option in + /// a `SHOW` statement before the `IN` option + fn supports_show_like_before_in(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index d9331d952..45ab33796 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -203,6 +203,12 @@ impl Dialect for SnowflakeDialect { fn allow_extract_single_quotes(&self) -> bool { true } + + /// Snowflake expects the `LIKE` option before the `IN` option, + /// for example: https://docs.snowflake.com/en/sql-reference/sql/show-views#syntax + fn supports_show_like_before_in(&self) -> bool { + true + } } /// Parse snowflake create table statement. diff --git a/src/keywords.rs b/src/keywords.rs index 982cea81b..0f9195cfb 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -76,6 +76,7 @@ define_keywords!( ABS, ABSOLUTE, ACCESS, + ACCOUNT, ACTION, ADD, ADMIN, @@ -710,6 +711,7 @@ define_keywords!( STABLE, STAGE, START, + STARTS, STATEMENT, STATIC, STATISTICS, @@ -746,6 +748,7 @@ define_keywords!( TEMP, TEMPORARY, TERMINATED, + TERSE, TEXT, TEXTFILE, THEN, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 355456d52..d1d8173da 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9605,21 +9605,23 @@ impl<'a> Parser<'a> { } pub fn parse_show(&mut self) -> Result { + let terse = self.parse_keyword(Keyword::TERSE); let extended = self.parse_keyword(Keyword::EXTENDED); let full = self.parse_keyword(Keyword::FULL); let session = self.parse_keyword(Keyword::SESSION); let global = self.parse_keyword(Keyword::GLOBAL); + let external = self.parse_keyword(Keyword::EXTERNAL); if self .parse_one_of_keywords(&[Keyword::COLUMNS, Keyword::FIELDS]) .is_some() { Ok(self.parse_show_columns(extended, full)?) } else if self.parse_keyword(Keyword::TABLES) { - Ok(self.parse_show_tables(extended, full)?) + Ok(self.parse_show_tables(terse, extended, full, external)?) } else if self.parse_keywords(&[Keyword::MATERIALIZED, Keyword::VIEWS]) { - Ok(self.parse_show_views(true)?) + Ok(self.parse_show_views(terse, true)?) } else if self.parse_keyword(Keyword::VIEWS) { - Ok(self.parse_show_views(false)?) + Ok(self.parse_show_views(terse, false)?) } else if self.parse_keyword(Keyword::FUNCTIONS) { Ok(self.parse_show_functions()?) } else if extended || full { @@ -9647,9 +9649,9 @@ impl<'a> Parser<'a> { global, }) } else if self.parse_keyword(Keyword::DATABASES) { - self.parse_show_databases() + self.parse_show_databases(terse) } else if self.parse_keyword(Keyword::SCHEMAS) { - self.parse_show_schemas() + self.parse_show_schemas(terse) } else { Ok(Statement::ShowVariable { variable: self.parse_identifiers()?, @@ -9657,15 +9659,39 @@ impl<'a> Parser<'a> { } } - fn parse_show_databases(&mut self) -> Result { + fn parse_show_databases(&mut self, terse: bool) -> Result { + let history = self.parse_keyword(Keyword::HISTORY); + let filter = self.parse_show_statement_filter()?; + let show_in = self.parse_show_opt_in()?; + let starts_with = self.parse_show_opt_starts_with()?; + let limit = self.parse_show_opt_limit()?; + let from = self.parse_show_opt_from()?; Ok(Statement::ShowDatabases { - filter: self.parse_show_statement_filter()?, + terse, + history, + filter, + show_in, + starts_with, + limit, + from, }) } - fn parse_show_schemas(&mut self) -> Result { + fn parse_show_schemas(&mut self, terse: bool) -> Result { + let history = self.parse_keyword(Keyword::HISTORY); + let filter = self.parse_show_statement_filter()?; + let show_in = self.parse_show_opt_in()?; + let starts_with = self.parse_show_opt_starts_with()?; + let limit = self.parse_show_opt_limit()?; + let from = self.parse_show_opt_from()?; Ok(Statement::ShowSchemas { - filter: self.parse_show_statement_filter()?, + terse, + history, + filter, + show_in, + starts_with, + limit, + from, }) } @@ -9699,58 +9725,95 @@ impl<'a> Parser<'a> { extended: bool, full: bool, ) -> Result { - self.expect_one_of_keywords(&[Keyword::FROM, Keyword::IN])?; - let object_name = self.parse_object_name(false)?; - let table_name = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { - Some(_) => { - let db_name = vec![self.parse_identifier(false)?]; - let ObjectName(table_name) = object_name; - let object_name = db_name.into_iter().chain(table_name).collect(); - ObjectName(object_name) - } - None => object_name, - }; - let filter = self.parse_show_statement_filter()?; + let filter; + let filter_position; + let show_in; + if self.dialect.supports_show_like_before_in() { + filter = self.parse_show_statement_filter()?; + filter_position = ShowStatementFilterPosition::InTheMiddle; + show_in = self.parse_show_opt_in()?; + } else { + show_in = self.parse_show_opt_in()?; + filter = self.parse_show_statement_filter()?; + filter_position = ShowStatementFilterPosition::AtTheEnd; + } Ok(Statement::ShowColumns { extended, full, - table_name, + show_in, filter, + filter_position, }) } - pub fn parse_show_tables( + fn parse_show_tables( &mut self, + terse: bool, extended: bool, full: bool, + external: bool, ) -> Result { - let (clause, db_name) = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { - Some(Keyword::FROM) => (Some(ShowClause::FROM), Some(self.parse_identifier(false)?)), - Some(Keyword::IN) => (Some(ShowClause::IN), Some(self.parse_identifier(false)?)), - _ => (None, None), - }; - let filter = self.parse_show_statement_filter()?; + let history = !external && self.parse_keyword(Keyword::HISTORY); + let filter; + let show_in; + let filter_position; + if self.dialect.supports_show_like_before_in() { + filter = self.parse_show_statement_filter()?; + //YOAV: here we have a problem, the hint is DB-dependent (table in a schemas or some other object) + show_in = self.parse_show_opt_in()?; + filter_position = ShowStatementFilterPosition::InTheMiddle; + } else { + show_in = self.parse_show_opt_in()?; + filter = self.parse_show_statement_filter()?; + filter_position = ShowStatementFilterPosition::AtTheEnd; + } + let starts_with = self.parse_show_opt_starts_with()?; + let limit = self.parse_show_opt_limit()?; + let from = self.parse_show_opt_from()?; Ok(Statement::ShowTables { + terse, + history, extended, full, - clause, - db_name, + external, filter, + show_in, + starts_with, + limit, + from, + filter_position, }) } - fn parse_show_views(&mut self, materialized: bool) -> Result { - let (clause, db_name) = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { - Some(Keyword::FROM) => (Some(ShowClause::FROM), Some(self.parse_identifier(false)?)), - Some(Keyword::IN) => (Some(ShowClause::IN), Some(self.parse_identifier(false)?)), - _ => (None, None), - }; - let filter = self.parse_show_statement_filter()?; + fn parse_show_views( + &mut self, + terse: bool, + materialized: bool, + ) -> Result { + let filter; + let show_in; + let filter_position; + if self.dialect.supports_show_like_before_in() { + filter = self.parse_show_statement_filter()?; + show_in = self.parse_show_opt_in()?; + filter_position = ShowStatementFilterPosition::InTheMiddle; + } else { + show_in = self.parse_show_opt_in()?; + filter = self.parse_show_statement_filter()?; + filter_position = ShowStatementFilterPosition::AtTheEnd; + } + let starts_with = self.parse_show_opt_starts_with()?; + let limit = self.parse_show_opt_limit()?; + let from = self.parse_show_opt_from()?; Ok(Statement::ShowViews { materialized, - clause, - db_name, + terse, filter, + filter_position, + show_in, + starts_with, + limit, + from, }) } @@ -12389,6 +12452,137 @@ impl<'a> Parser<'a> { } false } + + /// Look for an expected keyword, without consuming it + fn peek_keyword(&self, expected: Keyword) -> bool { + match self.peek_token().token { + Token::Word(w) => expected == w.keyword, + _ => false, + } + } + + /// Look for one of expected keyword, without consuming it + fn peek_keywords(&self, expected: &[Keyword]) -> bool { + for kw in expected { + if self.peek_keyword(*kw) { + return true; + } + } + false + } + + fn parse_show_opt_in(&mut self) -> Result, ParserError> { + let clause = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { + Some(Keyword::FROM) => ShowStatementInClause::FROM, + Some(Keyword::IN) => ShowStatementInClause::IN, + _ => return Ok(None), + }; + + if self.parse_keyword(Keyword::DATABASE) { + if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) { + Ok(Some(ShowStatementIn { + clause, + parent_type: Some(ShowStatementInParentType::Database), + parent_name: None, + })) + } else { + let parent_name = match self.parse_object_name(false) { + Ok(n) => Some(n), + Err(_) => None, + }; + Ok(Some(ShowStatementIn { + clause, + parent_type: Some(ShowStatementInParentType::Database), + parent_name, + })) + } + } else if self.parse_keyword(Keyword::SCHEMA) { + if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) { + Ok(Some(ShowStatementIn { + clause, + parent_type: Some(ShowStatementInParentType::Schema), + parent_name: None, + })) + } else { + let parent_name = match self.parse_object_name(false) { + Ok(n) => Some(n), + Err(_) => None, + }; + Ok(Some(ShowStatementIn { + clause, + parent_type: Some(ShowStatementInParentType::Schema), + parent_name, + })) + } + } else if self.parse_keyword(Keyword::ACCOUNT) { + let parent_name = match self.parse_object_name(false) { + Ok(n) => Some(n), + Err(_) => None, + }; + Ok(Some(ShowStatementIn { + clause, + parent_type: Some(ShowStatementInParentType::Account), + parent_name, + })) + } else if self.parse_keyword(Keyword::TABLE) { + let parent_name = match self.parse_object_name(false) { + Ok(n) => Some(n), + Err(_) => None, + }; + Ok(Some(ShowStatementIn { + clause, + parent_type: Some(ShowStatementInParentType::Table), + parent_name, + })) + } else if self.parse_keyword(Keyword::VIEW) { + let parent_name = match self.parse_object_name(false) { + Ok(n) => Some(n), + Err(_) => None, + }; + Ok(Some(ShowStatementIn { + clause, + parent_type: Some(ShowStatementInParentType::View), + parent_name, + })) + } else { + // Parsing MySQL style FROM tbl_name FROM db_name + // which is equivalent to FROM tbl_name.db_name + let mut parent_name = self.parse_object_name(false)?; + if self + .parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) + .is_some() + { + parent_name.0.insert(0, self.parse_identifier(false)?); + } + + Ok(Some(ShowStatementIn { + clause, + parent_type: None, + parent_name: Some(parent_name), + })) + } + } + + fn parse_show_opt_starts_with(&mut self) -> Result, ParserError> { + match self.parse_keywords(&[Keyword::STARTS, Keyword::WITH]) { + true => Ok(Some(self.parse_value()?)), + false => Ok(None), + } + } + + fn parse_show_opt_limit(&mut self) -> Result, ParserError> { + match self.parse_keyword(Keyword::LIMIT) { + true => Ok(self.parse_limit()?), + false => Ok(None), + } + } + + fn parse_show_opt_from(&mut self) -> Result, ParserError> { + match self.parse_keyword(Keyword::FROM) { + true => Ok(Some(self.parse_value()?)), + false => Ok(None), + } + } } impl Word { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index bef0f535c..70d48884d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11399,19 +11399,43 @@ fn test_show_dbs_schemas_tables_views() { verified_stmt("SHOW DATABASES LIKE '%abc'"); verified_stmt("SHOW SCHEMAS"); verified_stmt("SHOW SCHEMAS LIKE '%abc'"); - verified_stmt("SHOW TABLES"); - verified_stmt("SHOW TABLES IN db1"); - verified_stmt("SHOW TABLES IN db1 'abc'"); - verified_stmt("SHOW VIEWS"); - verified_stmt("SHOW VIEWS IN db1"); - verified_stmt("SHOW VIEWS IN db1 'abc'"); - verified_stmt("SHOW VIEWS FROM db1"); - verified_stmt("SHOW VIEWS FROM db1 'abc'"); - verified_stmt("SHOW MATERIALIZED VIEWS"); - verified_stmt("SHOW MATERIALIZED VIEWS IN db1"); - verified_stmt("SHOW MATERIALIZED VIEWS IN db1 'abc'"); - verified_stmt("SHOW MATERIALIZED VIEWS FROM db1"); - verified_stmt("SHOW MATERIALIZED VIEWS FROM db1 'abc'"); + + // SHOW is parsed the same by all dialects except the position of the LIKE option + all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt("SHOW TABLES"); + all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt("SHOW TABLES"); + all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt("SHOW TABLES IN db1"); + all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt("SHOW TABLES IN db1"); + all_dialects_where(|d| !d.supports_show_like_before_in()) + .verified_stmt("SHOW TABLES IN db1 'abc'"); + + all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt("SHOW VIEWS"); + all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt("SHOW VIEWS"); + all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt("SHOW VIEWS IN db1"); + all_dialects_where(|d| !d.supports_show_like_before_in()) + .verified_stmt("SHOW VIEWS IN db1 'abc'"); + all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt("SHOW VIEWS FROM db1"); + all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt("SHOW VIEWS FROM db1"); + all_dialects_where(|d| !d.supports_show_like_before_in()) + .verified_stmt("SHOW VIEWS FROM db1 'abc'"); + + all_dialects_where(|d| d.supports_show_like_before_in()) + .verified_stmt("SHOW MATERIALIZED VIEWS"); + all_dialects_where(|d| !d.supports_show_like_before_in()) + .verified_stmt("SHOW MATERIALIZED VIEWS"); + all_dialects_where(|d| d.supports_show_like_before_in()) + .verified_stmt("SHOW MATERIALIZED VIEWS IN db1"); + all_dialects_where(|d| !d.supports_show_like_before_in()) + .verified_stmt("SHOW MATERIALIZED VIEWS IN db1"); + all_dialects_where(|d| d.supports_show_like_before_in()) + .verified_stmt("SHOW MATERIALIZED VIEWS FROM db1"); + all_dialects_where(|d| !d.supports_show_like_before_in()) + .verified_stmt("SHOW MATERIALIZED VIEWS FROM db1"); + all_dialects_where(|d| !d.supports_show_like_before_in()) + .verified_stmt("SHOW MATERIALIZED VIEWS IN db1 'abc'"); + all_dialects_where(|d| !d.supports_show_like_before_in()) + .verified_stmt("SHOW MATERIALIZED VIEWS FROM db1"); + all_dialects_where(|d| !d.supports_show_like_before_in()) + .verified_stmt("SHOW MATERIALIZED VIEWS FROM db1 'abc'"); } #[test] diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 44b2ac6ba..2aaa89bb7 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -223,14 +223,18 @@ fn parse_flush() { #[test] fn parse_show_columns() { - let table_name = ObjectName(vec![Ident::new("mytable")]); assert_eq!( mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mytable"), Statement::ShowColumns { extended: false, full: false, - table_name: table_name.clone(), + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), filter: None, + filter_position: ShowStatementFilterPosition::AtTheEnd, } ); assert_eq!( @@ -238,8 +242,13 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: false, - table_name: ObjectName(vec![Ident::new("mydb"), Ident::new("mytable")]), + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mydb"), Ident::new("mytable")])), + }), filter: None, + filter_position: ShowStatementFilterPosition::AtTheEnd, } ); assert_eq!( @@ -247,8 +256,13 @@ fn parse_show_columns() { Statement::ShowColumns { extended: true, full: false, - table_name: table_name.clone(), + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), filter: None, + filter_position: ShowStatementFilterPosition::AtTheEnd, } ); assert_eq!( @@ -256,8 +270,13 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: true, - table_name: table_name.clone(), + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), filter: None, + filter_position: ShowStatementFilterPosition::AtTheEnd, } ); assert_eq!( @@ -265,8 +284,13 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: false, - table_name: table_name.clone(), + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), filter: Some(ShowStatementFilter::Like("pattern".into())), + filter_position: ShowStatementFilterPosition::AtTheEnd, } ); assert_eq!( @@ -274,18 +298,23 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: false, - table_name, + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), filter: Some(ShowStatementFilter::Where( mysql_and_generic().verified_expr("1 = 2") )), + filter_position: ShowStatementFilterPosition::AtTheEnd, } ); mysql_and_generic() .one_statement_parses_to("SHOW FIELDS FROM mytable", "SHOW COLUMNS FROM mytable"); mysql_and_generic() - .one_statement_parses_to("SHOW COLUMNS IN mytable", "SHOW COLUMNS FROM mytable"); + .one_statement_parses_to("SHOW COLUMNS IN mytable", "SHOW COLUMNS IN mytable"); mysql_and_generic() - .one_statement_parses_to("SHOW FIELDS IN mytable", "SHOW COLUMNS FROM mytable"); + .one_statement_parses_to("SHOW FIELDS IN mytable", "SHOW COLUMNS IN mytable"); mysql_and_generic().one_statement_parses_to( "SHOW COLUMNS FROM mytable FROM mydb", "SHOW COLUMNS FROM mydb.mytable", @@ -327,63 +356,103 @@ fn parse_show_tables() { assert_eq!( mysql_and_generic().verified_stmt("SHOW TABLES"), Statement::ShowTables { + terse: false, + history: false, extended: false, full: false, - clause: None, - db_name: None, + external: false, + starts_with: None, + limit: None, + from: None, + show_in: None, filter: None, + filter_position: ShowStatementFilterPosition::AtTheEnd } ); assert_eq!( mysql_and_generic().verified_stmt("SHOW TABLES FROM mydb"), Statement::ShowTables { + terse: false, + history: false, extended: false, full: false, - clause: Some(ShowClause::FROM), - db_name: Some(Ident::new("mydb")), + external: false, + starts_with: None, + limit: None, + from: None, + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mydb")])), + }), filter: None, + filter_position: ShowStatementFilterPosition::AtTheEnd } ); assert_eq!( mysql_and_generic().verified_stmt("SHOW EXTENDED TABLES"), Statement::ShowTables { + terse: false, + history: false, extended: true, full: false, - clause: None, - db_name: None, + external: false, + starts_with: None, + limit: None, + from: None, + show_in: None, filter: None, + filter_position: ShowStatementFilterPosition::AtTheEnd } ); assert_eq!( mysql_and_generic().verified_stmt("SHOW FULL TABLES"), Statement::ShowTables { + terse: false, + history: false, extended: false, full: true, - clause: None, - db_name: None, + external: false, + starts_with: None, + limit: None, + from: None, + show_in: None, filter: None, + filter_position: ShowStatementFilterPosition::AtTheEnd } ); assert_eq!( mysql_and_generic().verified_stmt("SHOW TABLES LIKE 'pattern'"), Statement::ShowTables { + terse: false, + history: false, extended: false, full: false, - clause: None, - db_name: None, + external: false, + starts_with: None, + limit: None, + from: None, + show_in: None, filter: Some(ShowStatementFilter::Like("pattern".into())), + filter_position: ShowStatementFilterPosition::AtTheEnd } ); assert_eq!( mysql_and_generic().verified_stmt("SHOW TABLES WHERE 1 = 2"), Statement::ShowTables { + terse: false, + history: false, extended: false, full: false, - clause: None, - db_name: None, + external: false, + starts_with: None, + limit: None, + from: None, + show_in: None, filter: Some(ShowStatementFilter::Where( mysql_and_generic().verified_expr("1 = 2") )), + filter_position: ShowStatementFilterPosition::AtTheEnd } ); mysql_and_generic().verified_stmt("SHOW TABLES IN mydb"); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index c17c7b958..5d70ac543 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2781,3 +2781,68 @@ fn test_parentheses_overflow() { snowflake_with_recursion_limit(max_nesting_level).parse_sql_statements(sql.as_str()); assert_eq!(parsed.err(), Some(ParserError::RecursionLimitExceeded)); } + +#[test] +fn test_show_databases() { + snowflake().verified_stmt("SHOW DATABASES"); + snowflake().verified_stmt("SHOW DATABASES HISTORY"); + snowflake().verified_stmt("SHOW DATABASES LIKE '%abc%'"); + snowflake().verified_stmt("SHOW DATABASES STARTS WITH 'demo_db'"); + snowflake().verified_stmt("SHOW DATABASES LIMIT 12"); + snowflake() + .verified_stmt("SHOW DATABASES HISTORY LIKE '%aa' STARTS WITH 'demo' LIMIT 20 FROM 'abc'"); + snowflake().verified_stmt("SHOW DATABASES IN ACCOUNT abc"); +} + +#[test] +fn test_parse_show_schemas() { + snowflake().verified_stmt("SHOW SCHEMAS"); + snowflake().verified_stmt("SHOW SCHEMAS IN ACCOUNT"); + snowflake().verified_stmt("SHOW SCHEMAS IN ACCOUNT abc"); + snowflake().verified_stmt("SHOW SCHEMAS IN DATABASE"); + snowflake().verified_stmt("SHOW SCHEMAS IN DATABASE xyz"); + snowflake().verified_stmt("SHOW SCHEMAS HISTORY LIKE '%xa%'"); + snowflake().verified_stmt("SHOW SCHEMAS STARTS WITH 'abc' LIMIT 20"); + snowflake().verified_stmt("SHOW SCHEMAS IN DATABASE STARTS WITH 'abc' LIMIT 20 FROM 'xyz'"); +} + +#[test] +fn test_parse_show_tables() { + snowflake().verified_stmt("SHOW TABLES"); + snowflake().verified_stmt("SHOW TABLES IN ACCOUNT"); + snowflake().verified_stmt("SHOW TABLES IN DATABASE"); + snowflake().verified_stmt("SHOW TABLES IN DATABASE xyz"); + snowflake().verified_stmt("SHOW TABLES IN SCHEMA"); + snowflake().verified_stmt("SHOW TABLES IN SCHEMA xyz"); + snowflake().verified_stmt("SHOW TABLES HISTORY LIKE '%xa%'"); + snowflake().verified_stmt("SHOW TABLES STARTS WITH 'abc' LIMIT 20"); + snowflake().verified_stmt("SHOW TABLES IN SCHEMA STARTS WITH 'abc' LIMIT 20 FROM 'xyz'"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES IN ACCOUNT"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES IN DATABASE"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES IN DATABASE xyz"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES IN SCHEMA"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES IN SCHEMA xyz"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES STARTS WITH 'abc' LIMIT 20"); + snowflake() + .verified_stmt("SHOW EXTERNAL TABLES IN SCHEMA STARTS WITH 'abc' LIMIT 20 FROM 'xyz'"); +} + +#[test] +fn test_show_views() { + snowflake().verified_stmt("SHOW VIEWS"); + snowflake().verified_stmt("SHOW VIEWS IN ACCOUNT"); + snowflake().verified_stmt("SHOW VIEWS IN DATABASE"); + snowflake().verified_stmt("SHOW VIEWS IN DATABASE xyz"); + snowflake().verified_stmt("SHOW VIEWS IN SCHEMA"); + snowflake().verified_stmt("SHOW VIEWS IN SCHEMA xyz"); + snowflake().verified_stmt("SHOW VIEWS STARTS WITH 'abc' LIMIT 20"); + snowflake().verified_stmt("SHOW VIEWS IN SCHEMA STARTS WITH 'abc' LIMIT 20 FROM 'xyz'"); +} + +#[test] +fn test_parse_show_columns_sql() { + snowflake().verified_stmt("SHOW COLUMNS IN TABLE abc"); + snowflake().verified_stmt("SHOW COLUMNS LIKE '%xyz%' IN TABLE abc"); + snowflake().verified_stmt("SHOW COLUMNS IN TABLE"); +} From 5a3af5722053c22d99149086646663293d775e2e Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Sat, 9 Nov 2024 19:05:02 +0100 Subject: [PATCH 2/6] Code review comments --- src/ast/mod.rs | 260 +++++++++-------------------------- src/parser/mod.rs | 275 ++++++++++++++------------------------ tests/sqlparser_common.rs | 78 +++++------ tests/sqlparser_mysql.rs | 206 ++++++++++++++++------------ 4 files changed, 319 insertions(+), 500 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 80faafc7d..ff9a516c6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2776,9 +2776,7 @@ pub enum Statement { ShowColumns { extended: bool, full: bool, - show_in: Option, - filter: Option, - filter_position: ShowStatementFilterPosition, + show_options: ShowStatementOptions, }, /// ```sql /// SHOW DATABASES @@ -2786,11 +2784,7 @@ pub enum Statement { ShowDatabases { terse: bool, history: bool, - filter: Option, - show_in: Option, - starts_with: Option, - limit: Option, - from: Option, + show_options: ShowStatementOptions, }, /// ```sql /// SHOW SCHEMAS @@ -2798,11 +2792,7 @@ pub enum Statement { ShowSchemas { terse: bool, history: bool, - filter: Option, - show_in: Option, - starts_with: Option, - limit: Option, - from: Option, + show_options: ShowStatementOptions, }, /// ```sql /// SHOW TABLES @@ -2813,12 +2803,7 @@ pub enum Statement { extended: bool, full: bool, external: bool, - filter: Option, - show_in: Option, - starts_with: Option, - limit: Option, - from: Option, - filter_position: ShowStatementFilterPosition, + show_options: ShowStatementOptions, }, /// ```sql /// SHOW VIEWS @@ -2826,12 +2811,7 @@ pub enum Statement { ShowViews { terse: bool, materialized: bool, - filter: Option, - show_in: Option, - starts_with: Option, - limit: Option, - from: Option, - filter_position: ShowStatementFilterPosition, + show_options: ShowStatementOptions, }, /// ```sql /// SHOW COLLATION @@ -4411,113 +4391,39 @@ impl fmt::Display for Statement { Statement::ShowColumns { extended, full, - show_in, - filter, - filter_position, + show_options, } => { write!( f, - "SHOW {extended}{full}COLUMNS", + "SHOW {extended}{full}COLUMNS{show_options}", extended = if *extended { "EXTENDED " } else { "" }, full = if *full { "FULL " } else { "" }, )?; - if filter_position == &ShowStatementFilterPosition::InTheMiddle { - if let Some(filter) = filter { - write!(f, " {filter}")?; - } - if let Some(show_in) = show_in { - write!(f, " {show_in}")?; - } - } - if filter_position == &ShowStatementFilterPosition::AtTheEnd { - if let Some(show_in) = show_in { - write!(f, " {show_in}")?; - } - if let Some(filter) = filter { - write!(f, " {filter}")?; - } - } Ok(()) } Statement::ShowDatabases { terse, history, - filter, - show_in, - starts_with, - limit, - from, + show_options, } => { write!( f, - "SHOW {terse}DATABASES", - terse = if *terse { "TERSE " } else { "" } - )?; - write!( - f, - "{history}{filter}{show_in}{starts_with}{limit}{from}", + "SHOW {terse}DATABASES{history}{show_options}", + terse = if *terse { "TERSE " } else { "" }, history = if *history { " HISTORY" } else { "" }, - filter = match filter.as_ref() { - Some(l) => format!(" {l}"), - None => String::new(), - }, - show_in = match show_in { - Some(i) => format!(" {i}"), - None => String::new(), - }, - starts_with = match starts_with.as_ref() { - Some(s) => format!(" STARTS WITH {s}"), - None => String::new(), - }, - limit = match limit.as_ref() { - Some(l) => format!(" LIMIT {l}"), - None => String::new(), - }, - from = match from.as_ref() { - Some(f) => format!(" FROM {f}"), - None => String::new(), - } )?; Ok(()) } Statement::ShowSchemas { terse, history, - filter, - show_in, - starts_with, - limit, - from, + show_options, } => { write!( f, - "SHOW {terse}SCHEMAS", - terse = if *terse { "TERSE " } else { "" } - )?; - write!( - f, - "{history}{filter}{show_in}{starts_with}{limit}{from}", + "SHOW {terse}SCHEMAS{history}{show_options}", + terse = if *terse { "TERSE " } else { "" }, history = if *history { " HISTORY" } else { "" }, - filter = match filter.as_ref() { - Some(l) => format!(" {l}"), - None => String::new(), - }, - show_in = match show_in { - Some(i) => format!(" {i}"), - None => String::new(), - }, - starts_with = match starts_with.as_ref() { - Some(s) => format!(" STARTS WITH {s}"), - None => String::new(), - }, - limit = match limit.as_ref() { - Some(l) => format!(" LIMIT {l}"), - None => String::new(), - }, - from = match from.as_ref() { - Some(f) => format!(" FROM {f}"), - None => String::new(), - } )?; Ok(()) } @@ -4527,110 +4433,30 @@ impl fmt::Display for Statement { extended, full, external, - filter, - show_in, - starts_with, - limit, - from, - filter_position, + show_options, } => { write!( f, - "SHOW {terse}{extended}{full}{external}TABLES", + "SHOW {terse}{extended}{full}{external}TABLES{history}{show_options}", terse = if *terse { "TERSE " } else { "" }, extended = if *extended { "EXTENDED " } else { "" }, full = if *full { "FULL " } else { "" }, external = if *external { "EXTERNAL " } else { "" }, - )?; - write!( - f, - "{history}{like_in_the_middle}{show_in}{starts_with}{limit}{from}{like_at_the_end}", history = if *history { " HISTORY" } else { "" }, - like_in_the_middle = if *filter_position - == ShowStatementFilterPosition::InTheMiddle - && filter.is_some() - { - format!(" {}", filter.as_ref().unwrap()) - } else { - String::new() - }, - show_in = match show_in { - Some(i) => format!(" {i}"), - None => String::new(), - }, - starts_with = match starts_with.as_ref() { - Some(s) => format!(" STARTS WITH {s}"), - None => String::new(), - }, - limit = match limit.as_ref() { - Some(l) => format!(" LIMIT {l}"), - None => String::new(), - }, - from = match from.as_ref() { - Some(f) => format!(" FROM {f}"), - None => String::new(), - }, - like_at_the_end = if *filter_position - == ShowStatementFilterPosition::AtTheEnd - && filter.is_some() - { - format!(" {}", filter.as_ref().unwrap()) - } else { - String::new() - }, - ) + )?; + Ok(()) } Statement::ShowViews { terse, materialized, - filter, - show_in, - starts_with, - limit, - from, - filter_position, + show_options, } => { write!( f, - "SHOW {terse}{materialized}VIEWS", + "SHOW {terse}{materialized}VIEWS{show_options}", terse = if *terse { "TERSE " } else { "" }, materialized = if *materialized { "MATERIALIZED " } else { "" } )?; - write!( - f, - "{like_in_the_middle}{show_in}{starts_with}{limit}{from}{like_at_the_end}", - like_in_the_middle = if *filter_position - == ShowStatementFilterPosition::InTheMiddle - && filter.is_some() - { - format!(" {}", filter.as_ref().unwrap()) - } else { - String::new() - }, - show_in = match show_in { - Some(i) => format!(" {i}"), - None => String::new(), - }, - starts_with = match starts_with.as_ref() { - Some(s) => format!(" STARTS WITH {s}"), - None => String::new(), - }, - limit = match limit.as_ref() { - Some(l) => format!(" LIMIT {l}"), - None => String::new(), - }, - from = match from.as_ref() { - Some(f) => format!(" FROM {f}"), - None => String::new(), - }, - like_at_the_end = if *filter_position == ShowStatementFilterPosition::AtTheEnd - && filter.is_some() - { - format!(" {}", filter.as_ref().unwrap()) - } else { - String::new() - } - )?; Ok(()) } Statement::ShowFunctions { filter } => { @@ -7528,10 +7354,54 @@ impl Display for UtilityOption { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +pub struct ShowStatementOptions { + pub show_in: Option, + pub starts_with: Option, + pub limit: Option, + pub limit_from: Option, + pub filter_position: Option, +} + +impl Display for ShowStatementOptions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (life_in_infix, like_in_suffix) = match &self.filter_position { + Some(ShowStatementFilterPosition::Infix(filter)) => { + (format!(" {filter}"), "".to_string()) + } + Some(ShowStatementFilterPosition::Suffix(filter)) => { + ("".to_string(), format!(" {filter}")) + } + None => ("".to_string(), "".to_string()), + }; + write!( + f, + "{life_in_infix}{show_in}{starts_with}{limit}{from}{like_in_suffix}", + show_in = match &self.show_in { + Some(i) => format!(" {i}"), + None => String::new(), + }, + starts_with = match &self.starts_with { + Some(s) => format!(" STARTS WITH {s}"), + None => String::new(), + }, + limit = match &self.limit { + Some(l) => format!(" LIMIT {l}"), + None => String::new(), + }, + from = match &self.limit_from { + Some(f) => format!(" FROM {f}"), + None => String::new(), + } + )?; + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] pub enum ShowStatementFilterPosition { - InTheMiddle, // Snowflake like - AtTheEnd, // MySQL like + Infix(ShowStatementFilter), // For example: SHOW COLUMNS LIKE '%name%' IN TABLE tbl + Suffix(ShowStatementFilter), // For example: SHOW COLUMNS IN tbl LIKE '%name%' } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d1d8173da..cfc66225a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9661,37 +9661,21 @@ impl<'a> Parser<'a> { fn parse_show_databases(&mut self, terse: bool) -> Result { let history = self.parse_keyword(Keyword::HISTORY); - let filter = self.parse_show_statement_filter()?; - let show_in = self.parse_show_opt_in()?; - let starts_with = self.parse_show_opt_starts_with()?; - let limit = self.parse_show_opt_limit()?; - let from = self.parse_show_opt_from()?; + let show_options = self.parse_show_stmt_options()?; Ok(Statement::ShowDatabases { terse, history, - filter, - show_in, - starts_with, - limit, - from, + show_options, }) } fn parse_show_schemas(&mut self, terse: bool) -> Result { let history = self.parse_keyword(Keyword::HISTORY); - let filter = self.parse_show_statement_filter()?; - let show_in = self.parse_show_opt_in()?; - let starts_with = self.parse_show_opt_starts_with()?; - let limit = self.parse_show_opt_limit()?; - let from = self.parse_show_opt_from()?; + let show_options = self.parse_show_stmt_options()?; Ok(Statement::ShowSchemas { terse, history, - filter, - show_in, - starts_with, - limit, - from, + show_options, }) } @@ -9725,24 +9709,11 @@ impl<'a> Parser<'a> { extended: bool, full: bool, ) -> Result { - let filter; - let filter_position; - let show_in; - if self.dialect.supports_show_like_before_in() { - filter = self.parse_show_statement_filter()?; - filter_position = ShowStatementFilterPosition::InTheMiddle; - show_in = self.parse_show_opt_in()?; - } else { - show_in = self.parse_show_opt_in()?; - filter = self.parse_show_statement_filter()?; - filter_position = ShowStatementFilterPosition::AtTheEnd; - } + let show_options = self.parse_show_stmt_options()?; Ok(Statement::ShowColumns { extended, full, - show_in, - filter, - filter_position, + show_options, }) } @@ -9754,34 +9725,14 @@ impl<'a> Parser<'a> { external: bool, ) -> Result { let history = !external && self.parse_keyword(Keyword::HISTORY); - let filter; - let show_in; - let filter_position; - if self.dialect.supports_show_like_before_in() { - filter = self.parse_show_statement_filter()?; - //YOAV: here we have a problem, the hint is DB-dependent (table in a schemas or some other object) - show_in = self.parse_show_opt_in()?; - filter_position = ShowStatementFilterPosition::InTheMiddle; - } else { - show_in = self.parse_show_opt_in()?; - filter = self.parse_show_statement_filter()?; - filter_position = ShowStatementFilterPosition::AtTheEnd; - } - let starts_with = self.parse_show_opt_starts_with()?; - let limit = self.parse_show_opt_limit()?; - let from = self.parse_show_opt_from()?; + let show_options = self.parse_show_stmt_options()?; Ok(Statement::ShowTables { terse, history, extended, full, external, - filter, - show_in, - starts_with, - limit, - from, - filter_position, + show_options, }) } @@ -9790,30 +9741,11 @@ impl<'a> Parser<'a> { terse: bool, materialized: bool, ) -> Result { - let filter; - let show_in; - let filter_position; - if self.dialect.supports_show_like_before_in() { - filter = self.parse_show_statement_filter()?; - show_in = self.parse_show_opt_in()?; - filter_position = ShowStatementFilterPosition::InTheMiddle; - } else { - show_in = self.parse_show_opt_in()?; - filter = self.parse_show_statement_filter()?; - filter_position = ShowStatementFilterPosition::AtTheEnd; - } - let starts_with = self.parse_show_opt_starts_with()?; - let limit = self.parse_show_opt_limit()?; - let from = self.parse_show_opt_from()?; + let show_options = self.parse_show_stmt_options()?; Ok(Statement::ShowViews { materialized, terse, - filter, - filter_position, - show_in, - starts_with, - limit, - from, + show_options, }) } @@ -12453,134 +12385,121 @@ impl<'a> Parser<'a> { false } - /// Look for an expected keyword, without consuming it - fn peek_keyword(&self, expected: Keyword) -> bool { - match self.peek_token().token { - Token::Word(w) => expected == w.keyword, - _ => false, + /// Look for all of the expected keywords in sequence, without consuming them + fn peek_keywords(&mut self, expected: &[Keyword]) -> bool { + let index = self.index; + for kw in expected { + if !self.parse_keyword(*kw) { + self.index = index; + return false; + } } + self.index = index; + true } - /// Look for one of expected keyword, without consuming it - fn peek_keywords(&self, expected: &[Keyword]) -> bool { - for kw in expected { - if self.peek_keyword(*kw) { - return true; + fn parse_show_stmt_options(&mut self) -> Result { + let show_in; + let mut filter_position = None; + if self.dialect.supports_show_like_before_in() { + if let Some(filter) = self.parse_show_statement_filter()? { + filter_position = Some(ShowStatementFilterPosition::Infix(filter)); + } + show_in = self.maybe_parse_show_stmt_in()?; + } else { + show_in = self.maybe_parse_show_stmt_in()?; + if let Some(filter) = self.parse_show_statement_filter()? { + filter_position = Some(ShowStatementFilterPosition::Suffix(filter)); } } - false + let starts_with = self.maybe_parse_show_stmt_starts_with()?; + let limit = self.maybe_parse_show_stmt_limit()?; + let from = self.maybe_parse_show_stmt_from()?; + Ok(ShowStatementOptions { + filter_position, + show_in, + starts_with, + limit, + limit_from: from, + }) } - fn parse_show_opt_in(&mut self) -> Result, ParserError> { + fn maybe_parse_show_stmt_in(&mut self) -> Result, ParserError> { let clause = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { Some(Keyword::FROM) => ShowStatementInClause::FROM, Some(Keyword::IN) => ShowStatementInClause::IN, _ => return Ok(None), }; - if self.parse_keyword(Keyword::DATABASE) { - if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) { - Ok(Some(ShowStatementIn { - clause, - parent_type: Some(ShowStatementInParentType::Database), - parent_name: None, - })) - } else { - let parent_name = match self.parse_object_name(false) { - Ok(n) => Some(n), - Err(_) => None, - }; - Ok(Some(ShowStatementIn { - clause, - parent_type: Some(ShowStatementInParentType::Database), - parent_name, - })) + let (parent_type, parent_name) = match self.parse_one_of_keywords(&[ + Keyword::ACCOUNT, + Keyword::DATABASE, + Keyword::SCHEMA, + Keyword::TABLE, + Keyword::VIEW, + ]) { + Some(Keyword::DATABASE) if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) => { + (Some(ShowStatementInParentType::Database), None) } - } else if self.parse_keyword(Keyword::SCHEMA) { - if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) { - Ok(Some(ShowStatementIn { - clause, - parent_type: Some(ShowStatementInParentType::Schema), - parent_name: None, - })) - } else { + Some(Keyword::SCHEMA) if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) => { + (Some(ShowStatementInParentType::Schema), None) + } + Some(parent_kw) => { let parent_name = match self.parse_object_name(false) { Ok(n) => Some(n), - Err(_) => None, + _ => None, }; - Ok(Some(ShowStatementIn { - clause, - parent_type: Some(ShowStatementInParentType::Schema), - parent_name, - })) + match parent_kw { + Keyword::ACCOUNT => (Some(ShowStatementInParentType::Account), parent_name), + Keyword::DATABASE => (Some(ShowStatementInParentType::Database), parent_name), + Keyword::SCHEMA => (Some(ShowStatementInParentType::Schema), parent_name), + Keyword::TABLE => (Some(ShowStatementInParentType::Table), parent_name), + Keyword::VIEW => (Some(ShowStatementInParentType::View), parent_name), + _ => unreachable!(), + } } - } else if self.parse_keyword(Keyword::ACCOUNT) { - let parent_name = match self.parse_object_name(false) { - Ok(n) => Some(n), - Err(_) => None, - }; - Ok(Some(ShowStatementIn { - clause, - parent_type: Some(ShowStatementInParentType::Account), - parent_name, - })) - } else if self.parse_keyword(Keyword::TABLE) { - let parent_name = match self.parse_object_name(false) { - Ok(n) => Some(n), - Err(_) => None, - }; - Ok(Some(ShowStatementIn { - clause, - parent_type: Some(ShowStatementInParentType::Table), - parent_name, - })) - } else if self.parse_keyword(Keyword::VIEW) { - let parent_name = match self.parse_object_name(false) { - Ok(n) => Some(n), - Err(_) => None, - }; - Ok(Some(ShowStatementIn { - clause, - parent_type: Some(ShowStatementInParentType::View), - parent_name, - })) - } else { - // Parsing MySQL style FROM tbl_name FROM db_name - // which is equivalent to FROM tbl_name.db_name - let mut parent_name = self.parse_object_name(false)?; - if self - .parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) - .is_some() - { - parent_name.0.insert(0, self.parse_identifier(false)?); + None => { + // Parsing MySQL style FROM tbl_name FROM db_name + // which is equivalent to FROM tbl_name.db_name + let mut parent_name = self.parse_object_name(false)?; + if self + .parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) + .is_some() + { + parent_name.0.insert(0, self.parse_identifier(false)?); + } + (None, Some(parent_name)) } + }; - Ok(Some(ShowStatementIn { - clause, - parent_type: None, - parent_name: Some(parent_name), - })) - } + Ok(Some(ShowStatementIn { + clause, + parent_type, + parent_name, + })) } - fn parse_show_opt_starts_with(&mut self) -> Result, ParserError> { - match self.parse_keywords(&[Keyword::STARTS, Keyword::WITH]) { - true => Ok(Some(self.parse_value()?)), - false => Ok(None), + fn maybe_parse_show_stmt_starts_with(&mut self) -> Result, ParserError> { + if self.parse_keywords(&[Keyword::STARTS, Keyword::WITH]) { + Ok(Some(self.parse_value()?)) + } else { + Ok(None) } } - fn parse_show_opt_limit(&mut self) -> Result, ParserError> { - match self.parse_keyword(Keyword::LIMIT) { - true => Ok(self.parse_limit()?), - false => Ok(None), + fn maybe_parse_show_stmt_limit(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::LIMIT) { + Ok(self.parse_limit()?) + } else { + Ok(None) } } - fn parse_show_opt_from(&mut self) -> Result, ParserError> { - match self.parse_keyword(Keyword::FROM) { - true => Ok(Some(self.parse_value()?)), - false => Ok(None), + fn maybe_parse_show_stmt_from(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::FROM) { + Ok(Some(self.parse_value()?)) + } else { + Ok(None) } } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 70d48884d..d08e19d68 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11395,47 +11395,43 @@ fn test_try_convert() { #[test] fn test_show_dbs_schemas_tables_views() { - verified_stmt("SHOW DATABASES"); - verified_stmt("SHOW DATABASES LIKE '%abc'"); - verified_stmt("SHOW SCHEMAS"); - verified_stmt("SHOW SCHEMAS LIKE '%abc'"); - - // SHOW is parsed the same by all dialects except the position of the LIKE option - all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt("SHOW TABLES"); - all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt("SHOW TABLES"); - all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt("SHOW TABLES IN db1"); - all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt("SHOW TABLES IN db1"); - all_dialects_where(|d| !d.supports_show_like_before_in()) - .verified_stmt("SHOW TABLES IN db1 'abc'"); - - all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt("SHOW VIEWS"); - all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt("SHOW VIEWS"); - all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt("SHOW VIEWS IN db1"); - all_dialects_where(|d| !d.supports_show_like_before_in()) - .verified_stmt("SHOW VIEWS IN db1 'abc'"); - all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt("SHOW VIEWS FROM db1"); - all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt("SHOW VIEWS FROM db1"); - all_dialects_where(|d| !d.supports_show_like_before_in()) - .verified_stmt("SHOW VIEWS FROM db1 'abc'"); - - all_dialects_where(|d| d.supports_show_like_before_in()) - .verified_stmt("SHOW MATERIALIZED VIEWS"); - all_dialects_where(|d| !d.supports_show_like_before_in()) - .verified_stmt("SHOW MATERIALIZED VIEWS"); - all_dialects_where(|d| d.supports_show_like_before_in()) - .verified_stmt("SHOW MATERIALIZED VIEWS IN db1"); - all_dialects_where(|d| !d.supports_show_like_before_in()) - .verified_stmt("SHOW MATERIALIZED VIEWS IN db1"); - all_dialects_where(|d| d.supports_show_like_before_in()) - .verified_stmt("SHOW MATERIALIZED VIEWS FROM db1"); - all_dialects_where(|d| !d.supports_show_like_before_in()) - .verified_stmt("SHOW MATERIALIZED VIEWS FROM db1"); - all_dialects_where(|d| !d.supports_show_like_before_in()) - .verified_stmt("SHOW MATERIALIZED VIEWS IN db1 'abc'"); - all_dialects_where(|d| !d.supports_show_like_before_in()) - .verified_stmt("SHOW MATERIALIZED VIEWS FROM db1"); - all_dialects_where(|d| !d.supports_show_like_before_in()) - .verified_stmt("SHOW MATERIALIZED VIEWS FROM db1 'abc'"); + // These statements are parsed the same by all dialects + let stmts = vec![ + "SHOW DATABASES", + "SHOW SCHEMAS", + "SHOW TABLES", + "SHOW VIEWS", + "SHOW TABLES IN db1", + "SHOW VIEWS FROM db1", + "SHOW MATERIALIZED VIEWS", + "SHOW MATERIALIZED VIEWS IN db1", + "SHOW MATERIALIZED VIEWS FROM db1", + ]; + for stmt in stmts { + verified_stmt(stmt); + } + + // These statements are parsed the same by all dialects + // except for how the parser interprets the location of + // LIKE option (infix/suffix) + let stmts = vec!["SHOW DATABASES LIKE '%abc'", "SHOW SCHEMAS LIKE '%abc'"]; + for stmt in stmts { + all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt(stmt); + all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt(stmt); + } + + // These statements are only parsed by dialects that + // support the LIKE option in the suffix + let stmts = vec![ + "SHOW TABLES IN db1 'abc'", + "SHOW VIEWS IN db1 'abc'", + "SHOW VIEWS FROM db1 'abc'", + "SHOW MATERIALIZED VIEWS IN db1 'abc'", + "SHOW MATERIALIZED VIEWS FROM db1 'abc'", + ]; + for stmt in stmts { + all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt(stmt); + } } #[test] diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 2aaa89bb7..8269eadc0 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -228,13 +228,17 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: false, - show_in: Some(ShowStatementIn { - clause: ShowStatementInClause::FROM, - parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), - }), - filter: None, - filter_position: ShowStatementFilterPosition::AtTheEnd, + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), + filter_position: None, + limit_from: None, + limit: None, + starts_with: None, + } } ); assert_eq!( @@ -242,13 +246,17 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: false, - show_in: Some(ShowStatementIn { - clause: ShowStatementInClause::FROM, - parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mydb"), Ident::new("mytable")])), - }), - filter: None, - filter_position: ShowStatementFilterPosition::AtTheEnd, + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mydb"), Ident::new("mytable")])), + }), + filter_position: None, + limit_from: None, + limit: None, + starts_with: None, + } } ); assert_eq!( @@ -256,13 +264,17 @@ fn parse_show_columns() { Statement::ShowColumns { extended: true, full: false, - show_in: Some(ShowStatementIn { - clause: ShowStatementInClause::FROM, - parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), - }), - filter: None, - filter_position: ShowStatementFilterPosition::AtTheEnd, + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), + filter_position: None, + limit_from: None, + limit: None, + starts_with: None, + } } ); assert_eq!( @@ -270,13 +282,17 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: true, - show_in: Some(ShowStatementIn { - clause: ShowStatementInClause::FROM, - parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), - }), - filter: None, - filter_position: ShowStatementFilterPosition::AtTheEnd, + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), + filter_position: None, + limit_from: None, + limit: None, + starts_with: None, + } } ); assert_eq!( @@ -284,13 +300,19 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: false, - show_in: Some(ShowStatementIn { - clause: ShowStatementInClause::FROM, - parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), - }), - filter: Some(ShowStatementFilter::Like("pattern".into())), - filter_position: ShowStatementFilterPosition::AtTheEnd, + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), + filter_position: Some(ShowStatementFilterPosition::Suffix( + ShowStatementFilter::Like("pattern".into()) + )), + limit_from: None, + limit: None, + starts_with: None, + } } ); assert_eq!( @@ -298,15 +320,19 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: false, - show_in: Some(ShowStatementIn { - clause: ShowStatementInClause::FROM, - parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), - }), - filter: Some(ShowStatementFilter::Where( - mysql_and_generic().verified_expr("1 = 2") - )), - filter_position: ShowStatementFilterPosition::AtTheEnd, + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), + filter_position: Some(ShowStatementFilterPosition::Suffix( + ShowStatementFilter::Where(mysql_and_generic().verified_expr("1 = 2")) + )), + limit_from: None, + limit: None, + starts_with: None, + } } ); mysql_and_generic() @@ -361,12 +387,13 @@ fn parse_show_tables() { extended: false, full: false, external: false, - starts_with: None, - limit: None, - from: None, - show_in: None, - filter: None, - filter_position: ShowStatementFilterPosition::AtTheEnd + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: None, + filter_position: None + } } ); assert_eq!( @@ -377,16 +404,17 @@ fn parse_show_tables() { extended: false, full: false, external: false, - starts_with: None, - limit: None, - from: None, - show_in: Some(ShowStatementIn { - clause: ShowStatementInClause::FROM, - parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mydb")])), - }), - filter: None, - filter_position: ShowStatementFilterPosition::AtTheEnd + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mydb")])), + }), + filter_position: None + } } ); assert_eq!( @@ -397,12 +425,13 @@ fn parse_show_tables() { extended: true, full: false, external: false, - starts_with: None, - limit: None, - from: None, - show_in: None, - filter: None, - filter_position: ShowStatementFilterPosition::AtTheEnd + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: None, + filter_position: None + } } ); assert_eq!( @@ -413,12 +442,13 @@ fn parse_show_tables() { extended: false, full: true, external: false, - starts_with: None, - limit: None, - from: None, - show_in: None, - filter: None, - filter_position: ShowStatementFilterPosition::AtTheEnd + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: None, + filter_position: None + } } ); assert_eq!( @@ -429,12 +459,15 @@ fn parse_show_tables() { extended: false, full: false, external: false, - starts_with: None, - limit: None, - from: None, - show_in: None, - filter: Some(ShowStatementFilter::Like("pattern".into())), - filter_position: ShowStatementFilterPosition::AtTheEnd + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: None, + filter_position: Some(ShowStatementFilterPosition::Suffix( + ShowStatementFilter::Like("pattern".into()) + )) + } } ); assert_eq!( @@ -445,14 +478,15 @@ fn parse_show_tables() { extended: false, full: false, external: false, - starts_with: None, - limit: None, - from: None, - show_in: None, - filter: Some(ShowStatementFilter::Where( - mysql_and_generic().verified_expr("1 = 2") - )), - filter_position: ShowStatementFilterPosition::AtTheEnd + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: None, + filter_position: Some(ShowStatementFilterPosition::Suffix( + ShowStatementFilter::Where(mysql_and_generic().verified_expr("1 = 2")) + )) + } } ); mysql_and_generic().verified_stmt("SHOW TABLES IN mydb"); From b2ceea9a34100b70e2d4c39237d4be786d9a1d05 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Sun, 10 Nov 2024 16:19:43 +0100 Subject: [PATCH 3/6] Code review comments --- src/ast/mod.rs | 7 +++-- src/keywords.rs | 1 + src/parser/mod.rs | 53 ++++++++++++++++++++++++------------ tests/sqlparser_snowflake.rs | 2 +- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ff9a516c6..2a025ed75 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7354,6 +7354,9 @@ impl Display for UtilityOption { } } +/// Represents the different options available for a SHOW +/// statement to filter the results. Example from Snowflake: +/// https://docs.snowflake.com/en/sql-reference/sql/show-tables #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] pub struct ShowStatementOptions { pub show_in: Option, @@ -7365,7 +7368,7 @@ pub struct ShowStatementOptions { impl Display for ShowStatementOptions { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let (life_in_infix, like_in_suffix) = match &self.filter_position { + let (like_in_infix, like_in_suffix) = match &self.filter_position { Some(ShowStatementFilterPosition::Infix(filter)) => { (format!(" {filter}"), "".to_string()) } @@ -7376,7 +7379,7 @@ impl Display for ShowStatementOptions { }; write!( f, - "{life_in_infix}{show_in}{starts_with}{limit}{from}{like_in_suffix}", + "{like_in_infix}{show_in}{starts_with}{limit}{from}{like_in_suffix}", show_in = match &self.show_in { Some(i) => format!(" {i}"), None => String::new(), diff --git a/src/keywords.rs b/src/keywords.rs index 0f9195cfb..9cdc90ce2 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -92,6 +92,7 @@ define_keywords!( AND, ANTI, ANY, + APPLICATION, APPLY, ARCHIVE, ARE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cfc66225a..d25a281b1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3205,6 +3205,22 @@ impl<'a> Parser<'a> { }) } + /// Look for all of the expected keywords in sequence, without consuming them + fn peek_keyword(&mut self, expected: Keyword) -> bool { + let index = self.index; + let matched = self.parse_keyword(expected); + self.index = index; + matched + } + + /// Look for all of the expected keywords in sequence, without consuming them + fn peek_keywords(&mut self, expected: &[Keyword]) -> bool { + let index = self.index; + let matched = self.parse_keywords(expected); + self.index = index; + matched + } + /// Return the first non-whitespace token that has not yet been processed /// (or None if reached end-of-file) and mark it as processed. OK to call /// repeatedly after reaching EOF. @@ -12385,19 +12401,6 @@ impl<'a> Parser<'a> { false } - /// Look for all of the expected keywords in sequence, without consuming them - fn peek_keywords(&mut self, expected: &[Keyword]) -> bool { - let index = self.index; - for kw in expected { - if !self.parse_keyword(*kw) { - self.index = index; - return false; - } - } - self.index = index; - true - } - fn parse_show_stmt_options(&mut self) -> Result { let show_in; let mut filter_position = None; @@ -12428,7 +12431,8 @@ impl<'a> Parser<'a> { let clause = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { Some(Keyword::FROM) => ShowStatementInClause::FROM, Some(Keyword::IN) => ShowStatementInClause::IN, - _ => return Ok(None), + None => return Ok(None), + _ => return self.expected("FROM or IN", self.peek_token()), }; let (parent_type, parent_name) = match self.parse_one_of_keywords(&[ @@ -12438,13 +12442,23 @@ impl<'a> Parser<'a> { Keyword::TABLE, Keyword::VIEW, ]) { - Some(Keyword::DATABASE) if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) => { + // If we see these next keywords it means we don't have a parent name + Some(Keyword::DATABASE) + if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) + | self.peek_keyword(Keyword::LIMIT) => + { (Some(ShowStatementInParentType::Database), None) } - Some(Keyword::SCHEMA) if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) => { + Some(Keyword::SCHEMA) + if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) + | self.peek_keyword(Keyword::LIMIT) => + { (Some(ShowStatementInParentType::Schema), None) } Some(parent_kw) => { + // The parent name here is still optional, for example: + // SHOW TABLES IN ACCOUNT, so parsing the object name + // may fail because the statement ends. let parent_name = match self.parse_object_name(false) { Ok(n) => Some(n), _ => None, @@ -12455,7 +12469,12 @@ impl<'a> Parser<'a> { Keyword::SCHEMA => (Some(ShowStatementInParentType::Schema), parent_name), Keyword::TABLE => (Some(ShowStatementInParentType::Table), parent_name), Keyword::VIEW => (Some(ShowStatementInParentType::View), parent_name), - _ => unreachable!(), + _ => { + return self.expected( + "one of ACCOUNT, DATABASE, SCHEMA, TABLE or VIEW", + self.peek_token(), + ) + } } } None => { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 5d70ac543..1f1c00e7a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2842,7 +2842,7 @@ fn test_show_views() { #[test] fn test_parse_show_columns_sql() { + snowflake().verified_stmt("SHOW COLUMNS IN TABLE"); snowflake().verified_stmt("SHOW COLUMNS IN TABLE abc"); snowflake().verified_stmt("SHOW COLUMNS LIKE '%xyz%' IN TABLE abc"); - snowflake().verified_stmt("SHOW COLUMNS IN TABLE"); } From 4d5661cf187edd12ddefa2379423614786f38f1d Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Tue, 12 Nov 2024 21:40:31 +0200 Subject: [PATCH 4/6] Code review comments --- src/ast/mod.rs | 2 ++ src/dialect/mod.rs | 2 +- src/parser/mod.rs | 9 +++------ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2a025ed75..034c2f65a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7358,6 +7358,8 @@ impl Display for UtilityOption { /// statement to filter the results. Example from Snowflake: /// https://docs.snowflake.com/en/sql-reference/sql/show-tables #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct ShowStatementOptions { pub show_in: Option, pub starts_with: Option, diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 3a1ff745d..f37c0d85c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -623,7 +623,7 @@ pub trait Dialect: Debug + Any { true } - /// Returns true if this dialect support the `LIKE 'pattern'` option in + /// Returns true if this dialect supports the `LIKE 'pattern'` option in /// a `SHOW` statement before the `IN` option fn supports_show_like_before_in(&self) -> bool { false diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d25a281b1..add12911b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12456,13 +12456,10 @@ impl<'a> Parser<'a> { (Some(ShowStatementInParentType::Schema), None) } Some(parent_kw) => { - // The parent name here is still optional, for example: - // SHOW TABLES IN ACCOUNT, so parsing the object name + // The parent name here is still optional, for example: + // SHOW TABLES IN ACCOUNT, so parsing the object name // may fail because the statement ends. - let parent_name = match self.parse_object_name(false) { - Ok(n) => Some(n), - _ => None, - }; + let parent_name = self.maybe_parse(|p| p.parse_object_name(false))?; match parent_kw { Keyword::ACCOUNT => (Some(ShowStatementInParentType::Account), parent_name), Keyword::DATABASE => (Some(ShowStatementInParentType::Database), parent_name), From 7c9c913a2231e3af11ee6d0003fe1b2dc12f3a58 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Wed, 13 Nov 2024 07:26:48 +0200 Subject: [PATCH 5/6] Fix build and docs --- src/ast/mod.rs | 10 +++++++--- src/dialect/snowflake.rs | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 034c2f65a..5d85830d8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7354,9 +7354,9 @@ impl Display for UtilityOption { } } -/// Represents the different options available for a SHOW -/// statement to filter the results. Example from Snowflake: -/// https://docs.snowflake.com/en/sql-reference/sql/show-tables +/// Represents the different options available for `SHOW` +/// statements to filter the results. Example from Snowflake: +/// #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -7404,6 +7404,8 @@ impl Display for ShowStatementOptions { } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ShowStatementFilterPosition { Infix(ShowStatementFilter), // For example: SHOW COLUMNS LIKE '%name%' IN TABLE tbl Suffix(ShowStatementFilter), // For example: SHOW COLUMNS IN tbl LIKE '%name%' @@ -7411,6 +7413,7 @@ pub enum ShowStatementFilterPosition { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ShowStatementInParentType { Account, Database, @@ -7433,6 +7436,7 @@ impl fmt::Display for ShowStatementInParentType { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct ShowStatementIn { pub clause: ShowStatementInClause, pub parent_type: Option, diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 45ab33796..98e8f5e2f 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -205,7 +205,7 @@ impl Dialect for SnowflakeDialect { } /// Snowflake expects the `LIKE` option before the `IN` option, - /// for example: https://docs.snowflake.com/en/sql-reference/sql/show-views#syntax + /// for example: fn supports_show_like_before_in(&self) -> bool { true } From c8757576ee97b1ef7260dbf21eb6e77b6055c66a Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Wed, 13 Nov 2024 08:08:10 +0200 Subject: [PATCH 6/6] FMT --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5d85830d8..848e6bdbd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7354,7 +7354,7 @@ impl Display for UtilityOption { } } -/// Represents the different options available for `SHOW` +/// Represents the different options available for `SHOW` /// statements to filter the results. Example from Snowflake: /// #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]