From 4eaf2acb5c97029b79eac64e8501537356a5a1ef Mon Sep 17 00:00:00 2001 From: sabir-akhadov-localstack Date: Tue, 1 Sep 2026 10:55:01 +0200 Subject: [PATCH 1/2] Snowflake: Add DESC[RIBE] EXTERNAL VOLUME Co-Authored-By: Claude Fable 5 --- src/ast/mod.rs | 32 +++++++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/snowflake.rs | 40 ++++++++++++++++++++++++++++++------ tests/sqlparser_snowflake.rs | 29 ++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 87618bbc5f..d5c5deda1e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4648,6 +4648,11 @@ pub enum Statement { comment: Option, }, /// ```sql + /// DESC[RIBE] EXTERNAL VOLUME + /// ``` + /// See + DescribeExternalVolume(DescribeExternalVolume), + /// ```sql /// CREATE [ OR REPLACE ] WAREHOUSE [ IF NOT EXISTS ] /// [ [ WITH ] = [ ... ] ] /// ``` @@ -6373,6 +6378,7 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::DescribeExternalVolume(s) => write!(f, "{s}"), Statement::CreateWarehouse(s) => write!(f, "{s}"), Statement::CopyIntoSnowflake { kind, @@ -11256,6 +11262,26 @@ pub struct ShowObjects { pub show_options: ShowStatementOptions, } +/// ```sql +/// DESC[RIBE] EXTERNAL VOLUME +/// ``` +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DescribeExternalVolume { + /// The keyword used, `DESC` or `DESCRIBE`. + pub describe_alias: DescribeAlias, + /// External volume name. + pub name: ObjectName, +} + +impl fmt::Display for DescribeExternalVolume { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} EXTERNAL VOLUME {}", self.describe_alias, self.name) + } +} + /// MSSQL's json null clause /// /// ```plaintext @@ -12751,6 +12777,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(d: DescribeExternalVolume) -> Self { + Self::DescribeExternalVolume(d) + } +} + impl From for Statement { fn from(c: CreateWarehouse) -> Self { Self::CreateWarehouse(c) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 74e0844d56..0c52c44048 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -529,6 +529,7 @@ impl Spanned for Statement { Statement::AlterUser(..) => Span::empty(), Statement::Reset(..) => Span::empty(), Statement::CreateAggregate(create_aggregate) => create_aggregate.span(), + Statement::DescribeExternalVolume(..) => Span::empty(), } } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index dc462f5ba2..818e73d7a1 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -29,12 +29,13 @@ use crate::ast::helpers::stmt_data_loading::{ use crate::ast::{ AlterTable, AlterTableOperation, AlterTableType, CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateTable, - CreateTableLikeKind, DollarQuotedString, Ident, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, InitializeKind, - Insert, MultiTableInsertIntoClause, MultiTableInsertType, MultiTableInsertValue, - MultiTableInsertValues, MultiTableInsertWhenClause, ObjectName, ObjectNamePart, - RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement, StorageLifecyclePolicy, - StorageSerializationPolicy, TableObject, TagsColumnOption, Value, WrappedCollection, + CreateTableLikeKind, DescribeAlias, DescribeExternalVolume, DollarQuotedString, Ident, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, InitializeKind, Insert, MultiTableInsertIntoClause, + MultiTableInsertType, MultiTableInsertValue, MultiTableInsertValues, + MultiTableInsertWhenClause, ObjectName, ObjectNamePart, RefreshModeKind, RowAccessPolicy, + ShowObjects, SqlOption, Statement, StorageLifecyclePolicy, StorageSerializationPolicy, + TableObject, TagsColumnOption, Value, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; @@ -312,6 +313,20 @@ impl Dialect for SnowflakeDialect { return Some(parse_alter_session(parser, set)); } + if let Some(kw) = parser.parse_one_of_keywords(&[Keyword::DESC, Keyword::DESCRIBE]) { + if parser.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) { + // DESC[RIBE] EXTERNAL VOLUME + let describe_alias = if kw == Keyword::DESC { + DescribeAlias::Desc + } else { + DescribeAlias::Describe + }; + return Some(parse_describe_external_volume(describe_alias, parser)); + } + // not EXTERNAL VOLUME — put back DESC/DESCRIBE + parser.prev_token(); + } + if parser.parse_keyword(Keyword::CREATE) { // possibly CREATE STAGE //[ OR REPLACE ] @@ -2031,3 +2046,16 @@ fn parse_multi_table_insert_when_clauses( Ok((when_clauses, else_clause)) } + +/// Parse `DESC[RIBE] EXTERNAL VOLUME ` +fn parse_describe_external_volume( + describe_alias: DescribeAlias, + parser: &mut Parser, +) -> Result { + let name = parser.parse_object_name(false)?; + Ok(DescribeExternalVolume { + describe_alias, + name, + } + .into()) +} diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 5e3cbc8e01..f50bc66159 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -5042,3 +5042,32 @@ fn test_stage_name_delimiters() { ParserError::ParserError("Expected: stage name identifier, found: .".to_string()), ); } + +#[test] +fn test_describe_external_volume() { + match snowflake().verified_stmt("DESCRIBE EXTERNAL VOLUME my_vol") { + Statement::DescribeExternalVolume(DescribeExternalVolume { + describe_alias, + name, + }) => { + assert_eq!(DescribeAlias::Describe, describe_alias); + assert_eq!("my_vol", name.to_string()); + } + _ => unreachable!(), + } +} + +#[test] +fn test_desc_external_volume() { + // The DESC spelling is preserved on round-trip. + match snowflake().verified_stmt("DESC EXTERNAL VOLUME my_vol") { + Statement::DescribeExternalVolume(DescribeExternalVolume { + describe_alias, + name, + }) => { + assert_eq!(DescribeAlias::Desc, describe_alias); + assert_eq!("my_vol", name.to_string()); + } + _ => unreachable!(), + } +} From 3902ada67b1b0c3b4300c0d90b63b2c856fbdfa8 Mon Sep 17 00:00:00 2001 From: sabir-akhadov-localstack Date: Thu, 15 Oct 2026 14:38:01 +0200 Subject: [PATCH 2/2] Add tests for DESC EXTERNAL VOLUME coverage Co-Authored-By: Claude Opus 5.5 --- tests/sqlparser_snowflake.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index f50bc66159..8512218d2c 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -5045,7 +5045,9 @@ fn test_stage_name_delimiters() { #[test] fn test_describe_external_volume() { - match snowflake().verified_stmt("DESCRIBE EXTERNAL VOLUME my_vol") { + let stmt = snowflake().verified_stmt("DESCRIBE EXTERNAL VOLUME my_vol"); + assert_eq!(Span::empty(), stmt.span()); + match stmt { Statement::DescribeExternalVolume(DescribeExternalVolume { describe_alias, name, @@ -5055,6 +5057,13 @@ fn test_describe_external_volume() { } _ => unreachable!(), } + + assert_eq!( + snowflake() + .parse_sql_statements("DESCRIBE EXTERNAL VOLUME") + .unwrap_err(), + ParserError::ParserError("Expected: identifier, found: EOF".to_string()) + ); } #[test]