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

github-bot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 203ced42 Fixed CHANGES keyword parsing for snowflake (#2266)
203ced42 is described below

commit 203ced42be28f4e418ca93a73fc75911be19f4b4
Author: Andriy Romanov <[email protected]>
AuthorDate: Fri Mar 13 05:53:57 2026 -0700

    Fixed CHANGES keyword parsing for snowflake (#2266)
---
 src/ast/query.rs             | 23 +++++++++++++++++++++++
 src/keywords.rs              |  1 +
 src/parser/mod.rs            | 26 ++++++++++++++++++++++++++
 tests/sqlparser_snowflake.rs | 31 ++++++++++++++++++++++++++++++-
 4 files changed, 80 insertions(+), 1 deletion(-)

diff --git a/src/ast/query.rs b/src/ast/query.rs
index 440928ed..ca74db44 100644
--- a/src/ast/query.rs
+++ b/src/ast/query.rs
@@ -2578,6 +2578,23 @@ pub enum TableVersion {
     /// When the table version is defined using a function.
     /// For example: `SELECT * FROM tbl AT(TIMESTAMP => '2020-08-14 09:30:00')`
     Function(Expr),
+    /// Snowflake `CHANGES` clause for change tracking queries.
+    /// For example:
+    /// ```sql
+    /// SELECT * FROM t
+    ///   CHANGES(INFORMATION => DEFAULT)
+    ///   AT(TIMESTAMP => TO_TIMESTAMP_TZ('...'))
+    ///   END(TIMESTAMP => TO_TIMESTAMP_TZ('...'))
+    /// ```
+    /// <https://docs.snowflake.com/en/sql-reference/constructs/changes>
+    Changes {
+        /// The `CHANGES(INFORMATION => ...)` function-call expression.
+        changes: Expr,
+        /// The `AT(TIMESTAMP => ...)` function-call expression.
+        at: Expr,
+        /// The optional `END(TIMESTAMP => ...)` function-call expression.
+        end: Option<Expr>,
+    },
 }
 
 impl Display for TableVersion {
@@ -2587,6 +2604,12 @@ impl Display for TableVersion {
             TableVersion::TimestampAsOf(e) => write!(f, "TIMESTAMP AS OF 
{e}")?,
             TableVersion::VersionAsOf(e) => write!(f, "VERSION AS OF {e}")?,
             TableVersion::Function(func) => write!(f, "{func}")?,
+            TableVersion::Changes { changes, at, end } => {
+                write!(f, "{changes} {at}")?;
+                if let Some(end) = end {
+                    write!(f, " {end}")?;
+                }
+            }
         }
         Ok(())
     }
diff --git a/src/keywords.rs b/src/keywords.rs
index de552bf2..d56d0484 100644
--- a/src/keywords.rs
+++ b/src/keywords.rs
@@ -202,6 +202,7 @@ define_keywords!(
     CENTURY,
     CHAIN,
     CHANGE,
+    CHANGES,
     CHANGE_TRACKING,
     CHANNEL,
     CHAR,
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 801f89e6..3bc34bda 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -16352,6 +16352,8 @@ impl<'a> Parser<'a> {
             {
                 let expr = self.parse_expr()?;
                 return Ok(Some(TableVersion::ForSystemTimeAsOf(expr)));
+            } else if self.peek_keyword(Keyword::CHANGES) {
+                return self.parse_table_version_changes().map(Some);
             } else if self.peek_keyword(Keyword::AT) || 
self.peek_keyword(Keyword::BEFORE) {
                 let func_name = self.parse_object_name(true)?;
                 let func = self.parse_function(func_name)?;
@@ -16367,6 +16369,30 @@ impl<'a> Parser<'a> {
         Ok(None)
     }
 
+    /// Parses the Snowflake `CHANGES` clause for change tracking queries.
+    ///
+    /// Syntax:
+    /// ```sql
+    /// CHANGES (INFORMATION => DEFAULT)
+    ///   AT (TIMESTAMP => <expr>)
+    ///   [END (TIMESTAMP => <expr>)]
+    /// ```
+    ///
+    /// <https://docs.snowflake.com/en/sql-reference/constructs/changes>
+    fn parse_table_version_changes(&mut self) -> Result<TableVersion, 
ParserError> {
+        let changes_name = self.parse_object_name(true)?;
+        let changes = self.parse_function(changes_name)?;
+        let at_name = self.parse_object_name(true)?;
+        let at = self.parse_function(at_name)?;
+        let end = if self.peek_keyword(Keyword::END) {
+            let end_name = self.parse_object_name(true)?;
+            Some(self.parse_function(end_name)?)
+        } else {
+            None
+        };
+        Ok(TableVersion::Changes { changes, at, end })
+    }
+
     /// Parses MySQL's JSON_TABLE column definition.
     /// For example: `id INT EXISTS PATH '$' DEFAULT '0' ON EMPTY ERROR ON 
ERROR`
     pub fn parse_json_table_column_def(&mut self) -> Result<JsonTableColumn, 
ParserError> {
diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs
index 5bb4a269..666876fa 100644
--- a/tests/sqlparser_snowflake.rs
+++ b/tests/sqlparser_snowflake.rs
@@ -3249,7 +3249,10 @@ fn parse_view_column_descriptions() {
 
 #[test]
 fn test_parentheses_overflow() {
-    let max_nesting_level: usize = 25;
+    // Use a modest nesting level to avoid actual stack overflow on
+    // CI runners with small thread stacks (debug builds use large frames
+    // and each nesting level adds extra depth via maybe_parse).
+    let max_nesting_level: usize = 20;
 
     // Verify the recursion check is not too wasteful (num of parentheses 
within budget)
     let slack = 3;
@@ -4004,6 +4007,32 @@ fn test_timetravel_at_before() {
         .verified_only_select("SELECT * FROM tbl BEFORE(TIMESTAMP => 
'2024-12-15 00:00:00')");
 }
 
+#[test]
+fn test_changes_clause() {
+    // CHANGES with AT and END
+    snowflake().verified_stmt(
+        r#"SELECT a FROM "PCH_ODS_FIDELIO"."SRC_VW_SYS_ACC_MASTER" 
CHANGES(INFORMATION => DEFAULT) AT(TIMESTAMP => TO_TIMESTAMP_TZ('2026-02-18 
11:23:19.660000000')) END(TIMESTAMP => TO_TIMESTAMP_TZ('2026-02-18 
11:38:30.211000000'))"#,
+    );
+
+    // CHANGES with AT only (no END)
+    snowflake().verified_stmt(
+        "SELECT a FROM t CHANGES(INFORMATION => DEFAULT) AT(TIMESTAMP => 
TO_TIMESTAMP_TZ('2026-02-18 11:23:19.660000000'))",
+    );
+
+    // CHANGES with APPEND_ONLY
+    snowflake().verified_stmt(
+        "SELECT a FROM t CHANGES(INFORMATION => APPEND_ONLY) AT(TIMESTAMP => 
TO_TIMESTAMP_TZ('2026-01-01 00:00:00'))",
+    );
+
+    // CHANGES with OFFSET
+    snowflake().verified_stmt("SELECT a FROM t CHANGES(INFORMATION => DEFAULT) 
AT(OFFSET => -60)");
+
+    // CHANGES with STATEMENT
+    snowflake().verified_stmt(
+        "SELECT a FROM t CHANGES(INFORMATION => DEFAULT) AT(STATEMENT => 
'8e5d0ca9-005e-44e6-b858-a8f5b37c5726')",
+    );
+}
+
 #[test]
 fn test_grant_account_global_privileges() {
     let privileges = vec![


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

Reply via email to