Skip to content

Commit 203ced4

Browse files
authored
Fixed CHANGES keyword parsing for snowflake (#2266)
1 parent 924a116 commit 203ced4

File tree

4 files changed

+80
-1
lines changed

4 files changed

+80
-1
lines changed

src/ast/query.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2578,6 +2578,23 @@ pub enum TableVersion {
25782578
/// When the table version is defined using a function.
25792579
/// For example: `SELECT * FROM tbl AT(TIMESTAMP => '2020-08-14 09:30:00')`
25802580
Function(Expr),
2581+
/// Snowflake `CHANGES` clause for change tracking queries.
2582+
/// For example:
2583+
/// ```sql
2584+
/// SELECT * FROM t
2585+
/// CHANGES(INFORMATION => DEFAULT)
2586+
/// AT(TIMESTAMP => TO_TIMESTAMP_TZ('...'))
2587+
/// END(TIMESTAMP => TO_TIMESTAMP_TZ('...'))
2588+
/// ```
2589+
/// <https://docs.snowflake.com/en/sql-reference/constructs/changes>
2590+
Changes {
2591+
/// The `CHANGES(INFORMATION => ...)` function-call expression.
2592+
changes: Expr,
2593+
/// The `AT(TIMESTAMP => ...)` function-call expression.
2594+
at: Expr,
2595+
/// The optional `END(TIMESTAMP => ...)` function-call expression.
2596+
end: Option<Expr>,
2597+
},
25812598
}
25822599

25832600
impl Display for TableVersion {
@@ -2587,6 +2604,12 @@ impl Display for TableVersion {
25872604
TableVersion::TimestampAsOf(e) => write!(f, "TIMESTAMP AS OF {e}")?,
25882605
TableVersion::VersionAsOf(e) => write!(f, "VERSION AS OF {e}")?,
25892606
TableVersion::Function(func) => write!(f, "{func}")?,
2607+
TableVersion::Changes { changes, at, end } => {
2608+
write!(f, "{changes} {at}")?;
2609+
if let Some(end) = end {
2610+
write!(f, " {end}")?;
2611+
}
2612+
}
25902613
}
25912614
Ok(())
25922615
}

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ define_keywords!(
202202
CENTURY,
203203
CHAIN,
204204
CHANGE,
205+
CHANGES,
205206
CHANGE_TRACKING,
206207
CHANNEL,
207208
CHAR,

src/parser/mod.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16352,6 +16352,8 @@ impl<'a> Parser<'a> {
1635216352
{
1635316353
let expr = self.parse_expr()?;
1635416354
return Ok(Some(TableVersion::ForSystemTimeAsOf(expr)));
16355+
} else if self.peek_keyword(Keyword::CHANGES) {
16356+
return self.parse_table_version_changes().map(Some);
1635516357
} else if self.peek_keyword(Keyword::AT) || self.peek_keyword(Keyword::BEFORE) {
1635616358
let func_name = self.parse_object_name(true)?;
1635716359
let func = self.parse_function(func_name)?;
@@ -16367,6 +16369,30 @@ impl<'a> Parser<'a> {
1636716369
Ok(None)
1636816370
}
1636916371

16372+
/// Parses the Snowflake `CHANGES` clause for change tracking queries.
16373+
///
16374+
/// Syntax:
16375+
/// ```sql
16376+
/// CHANGES (INFORMATION => DEFAULT)
16377+
/// AT (TIMESTAMP => <expr>)
16378+
/// [END (TIMESTAMP => <expr>)]
16379+
/// ```
16380+
///
16381+
/// <https://docs.snowflake.com/en/sql-reference/constructs/changes>
16382+
fn parse_table_version_changes(&mut self) -> Result<TableVersion, ParserError> {
16383+
let changes_name = self.parse_object_name(true)?;
16384+
let changes = self.parse_function(changes_name)?;
16385+
let at_name = self.parse_object_name(true)?;
16386+
let at = self.parse_function(at_name)?;
16387+
let end = if self.peek_keyword(Keyword::END) {
16388+
let end_name = self.parse_object_name(true)?;
16389+
Some(self.parse_function(end_name)?)
16390+
} else {
16391+
None
16392+
};
16393+
Ok(TableVersion::Changes { changes, at, end })
16394+
}
16395+
1637016396
/// Parses MySQL's JSON_TABLE column definition.
1637116397
/// For example: `id INT EXISTS PATH '$' DEFAULT '0' ON EMPTY ERROR ON ERROR`
1637216398
pub fn parse_json_table_column_def(&mut self) -> Result<JsonTableColumn, ParserError> {

tests/sqlparser_snowflake.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3249,7 +3249,10 @@ fn parse_view_column_descriptions() {
32493249

32503250
#[test]
32513251
fn test_parentheses_overflow() {
3252-
let max_nesting_level: usize = 25;
3252+
// Use a modest nesting level to avoid actual stack overflow on
3253+
// CI runners with small thread stacks (debug builds use large frames
3254+
// and each nesting level adds extra depth via maybe_parse).
3255+
let max_nesting_level: usize = 20;
32533256

32543257
// Verify the recursion check is not too wasteful (num of parentheses within budget)
32553258
let slack = 3;
@@ -4004,6 +4007,32 @@ fn test_timetravel_at_before() {
40044007
.verified_only_select("SELECT * FROM tbl BEFORE(TIMESTAMP => '2024-12-15 00:00:00')");
40054008
}
40064009

4010+
#[test]
4011+
fn test_changes_clause() {
4012+
// CHANGES with AT and END
4013+
snowflake().verified_stmt(
4014+
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'))"#,
4015+
);
4016+
4017+
// CHANGES with AT only (no END)
4018+
snowflake().verified_stmt(
4019+
"SELECT a FROM t CHANGES(INFORMATION => DEFAULT) AT(TIMESTAMP => TO_TIMESTAMP_TZ('2026-02-18 11:23:19.660000000'))",
4020+
);
4021+
4022+
// CHANGES with APPEND_ONLY
4023+
snowflake().verified_stmt(
4024+
"SELECT a FROM t CHANGES(INFORMATION => APPEND_ONLY) AT(TIMESTAMP => TO_TIMESTAMP_TZ('2026-01-01 00:00:00'))",
4025+
);
4026+
4027+
// CHANGES with OFFSET
4028+
snowflake().verified_stmt("SELECT a FROM t CHANGES(INFORMATION => DEFAULT) AT(OFFSET => -60)");
4029+
4030+
// CHANGES with STATEMENT
4031+
snowflake().verified_stmt(
4032+
"SELECT a FROM t CHANGES(INFORMATION => DEFAULT) AT(STATEMENT => '8e5d0ca9-005e-44e6-b858-a8f5b37c5726')",
4033+
);
4034+
}
4035+
40074036
#[test]
40084037
fn test_grant_account_global_privileges() {
40094038
let privileges = vec![

0 commit comments

Comments
 (0)