From 1b2839e228495e7b86d6ae8c779150d7b6a5dac3 Mon Sep 17 00:00:00 2001 From: sabir-akhadov-localstack Date: Tue, 1 Sep 2026 10:55:01 +0200 Subject: [PATCH] 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 4d86dd6a6..aac8ea58a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4558,6 +4558,11 @@ pub enum Statement { comment: Option, }, /// ```sql + /// DESC[RIBE] EXTERNAL VOLUME + /// ``` + /// See + DescribeExternalVolume(DescribeExternalVolume), + /// ```sql /// CREATE [ OR REPLACE ] WAREHOUSE [ IF NOT EXISTS ] /// [ [ WITH ] = [ ... ] ] /// ``` @@ -6293,6 +6298,7 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::DescribeExternalVolume(s) => write!(f, "{s}"), Statement::CreateWarehouse(s) => write!(f, "{s}"), Statement::CopyIntoSnowflake { kind, @@ -11131,6 +11137,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 @@ -12626,6 +12652,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 a34fe66d9..e299bb4f5 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -523,6 +523,7 @@ impl Spanned for Statement { Statement::Vacuum(..) => Span::empty(), Statement::AlterUser(..) => Span::empty(), Statement::Reset(..) => Span::empty(), + Statement::DescribeExternalVolume(..) => Span::empty(), } } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 0bedb12a5..07eb2bb85 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; @@ -286,6 +287,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 ] @@ -1988,3 +2003,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 059560dcc..e1f18d91f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4912,3 +4912,32 @@ fn test_select_dollar_column_from_stage() { // With table function args, without alias snowflake().verified_stmt("SELECT $1, $2 FROM @mystage1(file_format => 'myformat')"); } + +#[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!(), + } +}