This is an automated email from the ASF dual-hosted git repository.

github-bot pushed a commit to branch 
gh-readonly-queue/main/pr-2264-9d5a171a85b06ca7df083287027d8620007f909b
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git

commit 924a116a2edf58db23530d6fc09e19924709e60d
Author: Andriy Romanov <[email protected]>
AuthorDate: Fri Mar 13 05:43:46 2026 -0700

    Fix STORAGE LIFECYCLE POLICY for snowflake queries (#2264)
---
 src/ast/ddl.rs                       | 13 ++++++++++---
 src/ast/helpers/stmt_create_table.rs | 17 +++++++++++++++--
 src/ast/mod.rs                       | 24 ++++++++++++++++++++++++
 src/ast/spans.rs                     |  1 +
 src/dialect/snowflake.rs             | 16 +++++++++++++++-
 src/keywords.rs                      |  1 +
 tests/sqlparser_duckdb.rs            |  1 +
 tests/sqlparser_mssql.rs             |  2 ++
 tests/sqlparser_postgres.rs          |  1 +
 tests/sqlparser_snowflake.rs         | 26 ++++++++++++++++++++++++++
 10 files changed, 96 insertions(+), 6 deletions(-)

diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs
index 49dc5202..157b209d 100644
--- a/src/ast/ddl.rs
+++ b/src/ast/ddl.rs
@@ -48,9 +48,9 @@ use crate::ast::{
     HiveFormat, HiveIOFormat, HiveRowFormat, HiveSetLocation, Ident, 
InitializeKind,
     MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, 
OperateFunctionArg,
     OrderByExpr, ProjectionSelect, Query, RefreshModeKind, RowAccessPolicy, 
SequenceOptions,
-    Spanned, SqlOption, StorageSerializationPolicy, TableVersion, Tag, 
TriggerEvent,
-    TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, 
ValueWithSpan,
-    WrappedCollection,
+    Spanned, SqlOption, StorageLifecyclePolicy, StorageSerializationPolicy, 
TableVersion, Tag,
+    TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod, 
TriggerReferencing, Value,
+    ValueWithSpan, WrappedCollection,
 };
 use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, 
SpaceOrNewline};
 use crate::keywords::Keyword;
@@ -3012,6 +3012,9 @@ pub struct CreateTable {
     /// Snowflake "WITH ROW ACCESS POLICY" clause
     /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
     pub with_row_access_policy: Option<RowAccessPolicy>,
+    /// Snowflake `WITH STORAGE LIFECYCLE POLICY` clause
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
+    pub with_storage_lifecycle_policy: Option<StorageLifecyclePolicy>,
     /// Snowflake "WITH TAG" clause
     /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
     pub with_tags: Option<Vec<Tag>>,
@@ -3317,6 +3320,10 @@ impl fmt::Display for CreateTable {
             write!(f, " {row_access_policy}",)?;
         }
 
+        if let Some(storage_lifecycle_policy) = 
&self.with_storage_lifecycle_policy {
+            write!(f, " {storage_lifecycle_policy}",)?;
+        }
+
         if let Some(tag) = &self.with_tags {
             write!(f, " WITH TAG ({})", 
display_comma_separated(tag.as_slice()))?;
         }
diff --git a/src/ast/helpers/stmt_create_table.rs 
b/src/ast/helpers/stmt_create_table.rs
index 6af820e7..29589e21 100644
--- a/src/ast/helpers/stmt_create_table.rs
+++ b/src/ast/helpers/stmt_create_table.rs
@@ -28,8 +28,8 @@ use crate::ast::{
     ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableLikeKind, 
CreateTableOptions,
     DistStyle, Expr, FileFormat, ForValues, HiveDistributionStyle, HiveFormat, 
Ident,
     InitializeKind, ObjectName, OnCommit, OneOrManyWithParens, Query, 
RefreshModeKind,
-    RowAccessPolicy, Statement, StorageSerializationPolicy, TableConstraint, 
TableVersion, Tag,
-    WrappedCollection,
+    RowAccessPolicy, Statement, StorageLifecyclePolicy, 
StorageSerializationPolicy,
+    TableConstraint, TableVersion, Tag, WrappedCollection,
 };
 
 use crate::parser::ParserError;
@@ -149,6 +149,8 @@ pub struct CreateTableBuilder {
     pub with_aggregation_policy: Option<ObjectName>,
     /// Optional row access policy applied to the table.
     pub with_row_access_policy: Option<RowAccessPolicy>,
+    /// Optional storage lifecycle policy applied to the table.
+    pub with_storage_lifecycle_policy: Option<StorageLifecyclePolicy>,
     /// Optional tags/labels attached to the table metadata.
     pub with_tags: Option<Vec<Tag>>,
     /// Optional base location for staged data.
@@ -227,6 +229,7 @@ impl CreateTableBuilder {
             default_ddl_collation: None,
             with_aggregation_policy: None,
             with_row_access_policy: None,
+            with_storage_lifecycle_policy: None,
             with_tags: None,
             base_location: None,
             external_volume: None,
@@ -459,6 +462,14 @@ impl CreateTableBuilder {
         self.with_row_access_policy = with_row_access_policy;
         self
     }
+    /// Attach a storage lifecycle policy to the table.
+    pub fn with_storage_lifecycle_policy(
+        mut self,
+        with_storage_lifecycle_policy: Option<StorageLifecyclePolicy>,
+    ) -> Self {
+        self.with_storage_lifecycle_policy = with_storage_lifecycle_policy;
+        self
+    }
     /// Attach tags/labels to the table metadata.
     pub fn with_tags(mut self, with_tags: Option<Vec<Tag>>) -> Self {
         self.with_tags = with_tags;
@@ -582,6 +593,7 @@ impl CreateTableBuilder {
             default_ddl_collation: self.default_ddl_collation,
             with_aggregation_policy: self.with_aggregation_policy,
             with_row_access_policy: self.with_row_access_policy,
+            with_storage_lifecycle_policy: self.with_storage_lifecycle_policy,
             with_tags: self.with_tags,
             base_location: self.base_location,
             external_volume: self.external_volume,
@@ -661,6 +673,7 @@ impl From<CreateTable> for CreateTableBuilder {
             default_ddl_collation: table.default_ddl_collation,
             with_aggregation_policy: table.with_aggregation_policy,
             with_row_access_policy: table.with_row_access_policy,
+            with_storage_lifecycle_policy: table.with_storage_lifecycle_policy,
             with_tags: table.with_tags,
             base_location: table.base_location,
             external_volume: table.external_volume,
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index 6659878b..cff089bc 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -10472,6 +10472,30 @@ impl Display for RowAccessPolicy {
     }
 }
 
+/// Snowflake `[ WITH ] STORAGE LIFECYCLE POLICY <policy_name> ON ( <col_name> 
[ , ... ] )`
+///
+/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct StorageLifecyclePolicy {
+    /// The fully-qualified policy object name.
+    pub policy: ObjectName,
+    /// Column names the policy applies to.
+    pub on: Vec<Ident>,
+}
+
+impl Display for StorageLifecyclePolicy {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(
+            f,
+            "WITH STORAGE LIFECYCLE POLICY {} ON ({})",
+            self.policy,
+            display_comma_separated(self.on.as_slice())
+        )
+    }
+}
+
 /// Snowflake `WITH TAG ( tag_name = '<tag_value>', ...)`
 ///
 /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index 5777d289..74f731a7 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -572,6 +572,7 @@ impl Spanned for CreateTable {
             default_ddl_collation: _,           // string, no span
             with_aggregation_policy: _,         // todo, Snowflake specific
             with_row_access_policy: _,          // todo, Snowflake specific
+            with_storage_lifecycle_policy: _,   // todo, Snowflake specific
             with_tags: _,                       // todo, Snowflake specific
             external_volume: _,                 // todo, Snowflake specific
             base_location: _,                   // todo, Snowflake specific
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index f0f33f8e..416e5051 100644
--- a/src/dialect/snowflake.rs
+++ b/src/dialect/snowflake.rs
@@ -33,7 +33,7 @@ use crate::ast::{
     IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, 
InitializeKind,
     Insert, MultiTableInsertIntoClause, MultiTableInsertType, 
MultiTableInsertValue,
     MultiTableInsertValues, MultiTableInsertWhenClause, ObjectName, 
ObjectNamePart,
-    RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement,
+    RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement, 
StorageLifecyclePolicy,
     StorageSerializationPolicy, TableObject, TagsColumnOption, Value, 
WrappedCollection,
 };
 use crate::dialect::{Dialect, Precedence};
@@ -917,6 +917,7 @@ pub fn parse_create_table(
                 Keyword::WITH => {
                     parser.expect_one_of_keywords(&[
                         Keyword::AGGREGATION,
+                        Keyword::STORAGE,
                         Keyword::TAG,
                         Keyword::ROW,
                     ])?;
@@ -938,6 +939,19 @@ pub fn parse_create_table(
                     builder =
                         
builder.with_row_access_policy(Some(RowAccessPolicy::new(policy, columns)))
                 }
+                Keyword::STORAGE => {
+                    parser.expect_keywords(&[Keyword::LIFECYCLE, 
Keyword::POLICY])?;
+                    let policy = parser.parse_object_name(false)?;
+                    parser.expect_keyword_is(Keyword::ON)?;
+                    parser.expect_token(&Token::LParen)?;
+                    let columns = parser.parse_comma_separated(|p| 
p.parse_identifier())?;
+                    parser.expect_token(&Token::RParen)?;
+
+                    builder = 
builder.with_storage_lifecycle_policy(Some(StorageLifecyclePolicy {
+                        policy,
+                        on: columns,
+                    }))
+                }
                 Keyword::TAG => {
                     parser.expect_token(&Token::LParen)?;
                     let tags = 
parser.parse_comma_separated(Parser::parse_tag)?;
diff --git a/src/keywords.rs b/src/keywords.rs
index 9ea85fd3..de552bf2 100644
--- a/src/keywords.rs
+++ b/src/keywords.rs
@@ -573,6 +573,7 @@ define_keywords!(
     LEFT,
     LEFTARG,
     LEVEL,
+    LIFECYCLE,
     LIKE,
     LIKE_REGEX,
     LIMIT,
diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs
index b3c40761..671e92b9 100644
--- a/tests/sqlparser_duckdb.rs
+++ b/tests/sqlparser_duckdb.rs
@@ -776,6 +776,7 @@ fn test_duckdb_union_datatype() {
             default_ddl_collation: Default::default(),
             with_aggregation_policy: Default::default(),
             with_row_access_policy: Default::default(),
+            with_storage_lifecycle_policy: Default::default(),
             with_tags: Default::default(),
             base_location: Default::default(),
             external_volume: Default::default(),
diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs
index 7fc030ee..f2156e64 100644
--- a/tests/sqlparser_mssql.rs
+++ b/tests/sqlparser_mssql.rs
@@ -1994,6 +1994,7 @@ fn parse_create_table_with_valid_options() {
                 default_ddl_collation: None,
                 with_aggregation_policy: None,
                 with_row_access_policy: None,
+                with_storage_lifecycle_policy: None,
                 with_tags: None,
                 base_location: None,
                 external_volume: None,
@@ -2166,6 +2167,7 @@ fn parse_create_table_with_identity_column() {
                 default_ddl_collation: None,
                 with_aggregation_policy: None,
                 with_row_access_policy: None,
+                with_storage_lifecycle_policy: None,
                 with_tags: None,
                 base_location: None,
                 external_volume: None,
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index 2f74d706..9486af04 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -6462,6 +6462,7 @@ fn parse_trigger_related_functions() {
             default_ddl_collation: None,
             with_aggregation_policy: None,
             with_row_access_policy: None,
+            with_storage_lifecycle_policy: None,
             with_tags: None,
             base_location: None,
             external_volume: None,
diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs
index 0da44aa7..5bb4a269 100644
--- a/tests/sqlparser_snowflake.rs
+++ b/tests/sqlparser_snowflake.rs
@@ -286,6 +286,32 @@ fn test_snowflake_create_table_with_row_access_policy() {
     }
 }
 
+#[test]
+fn test_snowflake_create_table_with_storage_lifecycle_policy() {
+    // WITH keyword
+    match snowflake().verified_stmt(
+        "CREATE TABLE IF NOT EXISTS my_table (a NUMBER(38, 0), b VARIANT) WITH 
STORAGE LIFECYCLE POLICY dba.global_settings.my_policy ON (a)",
+    ) {
+        Statement::CreateTable(CreateTable {
+            name,
+            with_storage_lifecycle_policy,
+            ..
+        }) => {
+            assert_eq!("my_table", name.to_string());
+            let policy = with_storage_lifecycle_policy.unwrap();
+            assert_eq!("dba.global_settings.my_policy", 
policy.policy.to_string());
+            assert_eq!(vec![Ident::new("a")], policy.on);
+        }
+        _ => unreachable!(),
+    }
+
+    // Without WITH keyword — canonicalizes to WITH form
+    snowflake().one_statement_parses_to(
+        "CREATE TABLE my_table (a NUMBER(38, 0)) STORAGE LIFECYCLE POLICY 
my_policy ON (a, b)",
+        "CREATE TABLE my_table (a NUMBER(38, 0)) WITH STORAGE LIFECYCLE POLICY 
my_policy ON (a, b)",
+    );
+}
+
 #[test]
 fn test_snowflake_create_table_with_tag() {
     match snowflake()


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to