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]