Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(())
}
Expand Down
1 change: 1 addition & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ define_keywords!(
CENTURY,
CHAIN,
CHANGE,
CHANGES,
CHANGE_TRACKING,
CHANNEL,
CHAR,
Expand Down
26 changes: 26 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16305,6 +16305,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)?;
Expand All @@ -16320,6 +16322,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> {
Expand Down
31 changes: 30 additions & 1 deletion tests/sqlparser_snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3208,7 +3208,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;
Expand Down Expand Up @@ -3963,6 +3966,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![
Expand Down