Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fa4230e
feat(syntax): parse Snowflake external tables into structured nodes
kvlonge Nov 11, 2026
96c0fb4
feat(syntax): build Snowflake tables with policies as structured nodes
kvlonge Nov 11, 2026
1f6c848
feat(syntax): build Snowflake dynamic iceberg and interactive tables …
kvlonge Nov 11, 2026
d9de8c9
feat(syntax): check Snowflake event, event routing and online feature…
kvlonge Nov 11, 2026
77b29be
feat(syntax): check Snowflake iceberg and dynamic tables against thei…
kvlonge Nov 11, 2026
3e13ff4
feat(syntax): check Snowflake DCM table definitions against the table…
kvlonge Nov 11, 2026
708c74c
feat(syntax): accept Snowflake's scoped and volatile temporary tables
kvlonge Nov 11, 2026
cf8da2c
feat(syntax): build Snowflake table and view governance actions as nodes
kvlonge Nov 11, 2026
c291424
feat(syntax): parse Snowflake iceberg partition evolution
kvlonge Nov 11, 2026
ec8b63a
feat(syntax): read Snowflake ALTER TABLE ADD with the table grammar
kvlonge Nov 11, 2026
0499934
feat(syntax): parse Snowflake's key and unnamed constraint drops and …
kvlonge Nov 11, 2026
87c4373
feat(syntax): check Snowflake table search optimization targets
kvlonge Nov 11, 2026
97c9c72
feat(syntax): parse Snowflake iceberg column type and write default a…
kvlonge Nov 11, 2026
ba64962
feat(syntax): parse Snowflake table reclustering and iceberg refresh
kvlonge Nov 11, 2026
dc4653e
feat(syntax): read several Snowflake table properties after one SET
kvlonge Nov 11, 2026
1e130f5
fix(syntax): refuse misplaced Snowflake table actions
kvlonge Nov 11, 2026
008d9fa
feat(syntax): check Snowflake hybrid table indexes
kvlonge Nov 11, 2026
a80ca84
fix(syntax): read every secondary role of a Snowflake dynamic table
kvlonge Nov 11, 2026
46de9fc
fix(syntax): require a value for every Snowflake session setting
kvlonge Nov 11, 2026
1b9ee57
fix(syntax): parse Snowflake timestamp conversions with no arguments
kvlonge Nov 11, 2026
c1503f6
chore(syntax): re-record the Snowflake syntax baselines for the table…
kvlonge Nov 11, 2026
c6282cd
fix(syntax): keep the source range of Snowflake table column names
kvlonge Nov 11, 2026
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
5,156 changes: 3,724 additions & 1,432 deletions coverage/syntax/baseline.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions coverage/syntax/corpus-verdicts.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@
"refused": [
166,
270,
291,
294,
297,
300,
Expand Down Expand Up @@ -358,6 +359,8 @@
467,
468,
471,
482,
483,
492,
493,
501,
Expand All @@ -379,6 +382,7 @@
623,
624,
625,
626,
627,
629,
630,
Expand Down Expand Up @@ -5544,8 +5548,11 @@
645,
646,
708,
755,
756,
783,
784,
797,
807,
820,
821,
Expand Down Expand Up @@ -6079,6 +6086,7 @@
3513,
3514,
3518,
3564,
3604,
3747,
3751,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ impl Parser<'_> {
data_type,
using: None,
collate: None,
rename: None,
}));
}
if self.eat_kw("COMMENT") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ impl Parser<'_> {
data_type: data_type.unwrap_or(DataType::Unknown),
using,
collate,
rename: None,
})
}

Expand Down
49 changes: 40 additions & 9 deletions crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/alter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ impl Parser {
self.parse_alter_modify_action()
} else if self.match_identifier("CHANGE") {
self.parse_alter_change_action()
} else if self.match_token(TokenType::Constraint) {
} else if !self.is_snowflake() && self.match_token(TokenType::Constraint) {
let name = Some(self.expect_identifier_with_quoted()?);
let constraint = self.parse_constraint_definition(name)?;
Ok(AlterTableAction::AddConstraint(constraint))
} else if self.check(TokenType::PrimaryKey)
|| self.check(TokenType::ForeignKey)
|| self.check(TokenType::Unique)
} else if !self.is_snowflake()
&& (self.check(TokenType::PrimaryKey)
|| self.check(TokenType::ForeignKey)
|| self.check(TokenType::Unique))
{
let constraint = self.parse_table_constraint()?;
Ok(AlterTableAction::AddConstraint(constraint))
Expand Down Expand Up @@ -202,6 +203,9 @@ impl Parser {
}

fn parse_alter_rename_action(&mut self) -> Result<AlterTableAction> {
if let Some(action) = self.parse_snowflake_rename_constraint()? {
return Ok(action);
}
if self.match_token(TokenType::Index) || self.match_token(TokenType::Key) {
let old_name = self.expect_identifier_or_keyword_with_quoted()?;
self.expect_token(TokenType::To)?;
Expand Down Expand Up @@ -437,6 +441,9 @@ impl Parser {
}

fn parse_alter_set_action(&mut self) -> Result<AlterTableAction> {
if let Some(action) = self.parse_snowflake_table_set()? {
return Ok(action);
}
if self.check(TokenType::LParen) {
self.parse_alter_set_options()
} else if self.match_keyword("TAG") {
Expand Down Expand Up @@ -630,6 +637,9 @@ impl Parser {
}

fn parse_alter_drop_action(&mut self) -> Result<AlterTableAction> {
if let Some(action) = self.parse_snowflake_drop_key()? {
return Ok(action);
}
if self.check_identifier("CLUSTERING")
&& self.peek_nth(1).map(|t| t.text.eq_ignore_ascii_case("KEY")) == Some(true)
{
Expand Down Expand Up @@ -854,7 +864,9 @@ impl Parser {
sql: self.join_command_tokens(tokens),
});
}
if self.match_token(TokenType::Constraint) {
if let Some(action) = self.parse_snowflake_added_constraint()? {
Ok(action)
} else if self.match_token(TokenType::Constraint) {
let name = Some(self.expect_identifier_with_quoted()?);
let constraint = self.parse_constraint_definition(name)?;
Ok(AlterTableAction::AddConstraint(constraint))
Expand Down Expand Up @@ -933,6 +945,7 @@ impl Parser {
expression: None,
index_type: None,
granularity: None,
include_columns: Vec::new(),
}))
}

Expand Down Expand Up @@ -966,7 +979,7 @@ impl Parser {
location,
})
} else {
let col_def = self.parse_column_def()?;
let col_def = self.parse_added_column_def()?;
Ok(AlterTableAction::AddColumn {
column: col_def,
if_not_exists: true,
Expand Down Expand Up @@ -1015,7 +1028,7 @@ impl Parser {
fn parse_alter_add_column(&mut self) -> Result<AlterTableAction> {
let has_column_keyword = self.match_token(TokenType::Column);

if !has_column_keyword && self.check(TokenType::LParen) {
if !has_column_keyword && !self.is_snowflake() && self.check(TokenType::LParen) {
self.skip();
let mut columns: Vec<ColumnDef> = Vec::new();
loop {
Expand All @@ -1033,7 +1046,7 @@ impl Parser {
} else {
let if_not_exists =
self.match_keywords(&[TokenType::If, TokenType::Not, TokenType::Exists]);
let col_def = self.parse_column_def()?;
let col_def = self.parse_added_column_def()?;
let position = if self.match_token(TokenType::First) {
Some(ColumnPosition::First)
} else if self.match_token(TokenType::After) {
Expand Down Expand Up @@ -1237,6 +1250,14 @@ impl Parser {
self.current += 2;
return Ok(AlterColumnAction::SetDefault(self.parse_expression()?));
}
if self.match_text_seq(&["SET", "WRITE", "DEFAULT"])
|| self.match_text_seq(&["WRITE", "DEFAULT"])
{
return Ok(AlterColumnAction::SetWriteDefault(self.parse_expression()?));
}
if self.match_text_seq(&["DROP", "WRITE", "DEFAULT"]) {
return Ok(AlterColumnAction::DropWriteDefault);
}
if self.check_identifier("UNSET") {
return Err(self.parse_error("Expected COMMENT, a policy or TAG after UNSET"));
}
Expand All @@ -1251,7 +1272,10 @@ impl Parser {
let expr = self.parse_primary()?;
Ok(AlterColumnAction::SetDefault(expr))
} else if self.match_identifier("DATA") {
let _ = self.match_token(TokenType::Type) || self.match_identifier("TYPE");
let typed = self.match_token(TokenType::Type) || self.match_identifier("TYPE");
if !typed && self.is_snowflake() {
return Err(self.parse_error("Expected TYPE after SET DATA"));
}
let data_type = self.parse_data_type()?;
let collate = if self.match_token(TokenType::Collate) {
if self.check(TokenType::String) {
Expand All @@ -1268,10 +1292,16 @@ impl Parser {
} else {
None
};
let rename = match self.is_snowflake() && self.match_text_seq(&["RENAME"]) {
true if self.match_text_seq(&["FIELDS"]) => Some("RENAME FIELDS".to_string()),
true => Some("RENAME".to_string()),
false => None,
};
Ok(AlterColumnAction::SetDataType {
data_type,
using,
collate,
rename,
})
} else if self.match_identifier("VISIBLE") {
Ok(AlterColumnAction::SetVisible)
Expand Down Expand Up @@ -1315,6 +1345,7 @@ impl Parser {
data_type,
using,
collate,
rename: None,
})
} else {
Err(self.parse_error("Expected SET, DROP, or TYPE in ALTER COLUMN"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ impl Parser {
expression: None,
index_type: None,
granularity: None,
include_columns: Vec::new(),
})
} else {
let has_constraint_keyword = name.is_some();
Expand Down Expand Up @@ -491,6 +492,7 @@ impl Parser {
expression: None,
index_type: None,
granularity: None,
include_columns: Vec::new(),
})
}

Expand Down Expand Up @@ -1874,7 +1876,7 @@ impl Parser {
self.match_token(TokenType::Column);
let if_not_exists =
self.match_keywords(&[TokenType::If, TokenType::Not, TokenType::Exists]);
let col_def = self.parse_column_def()?;
let col_def = self.parse_added_column_def()?;
let position = if self.match_token(TokenType::First) {
Some(ColumnPosition::First)
} else if self.match_token(TokenType::After) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub(crate) mod explain;
pub(crate) mod scripting;
pub(crate) mod snowflake_alter;
pub(crate) mod snowflake_alter_account;
pub(crate) mod snowflake_alter_add;
pub(crate) mod snowflake_alter_dynamic;
pub(crate) mod snowflake_alter_governance;
pub(crate) mod snowflake_alter_objects;
Expand All @@ -23,6 +24,7 @@ pub(crate) mod snowflake_create_from;
pub(crate) mod snowflake_create_properties;
pub(crate) mod snowflake_create_table;
pub(crate) mod snowflake_data_metric;
pub(crate) mod snowflake_event_tables;
pub(crate) mod snowflake_execute_jobs;
pub(crate) mod snowflake_external_table;
pub(crate) mod snowflake_grant;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//! Snowflake's ALTER TABLE ADD of a column or an out-of-line constraint, read with the CREATE TABLE grammar.

use crate::parser::_helpers::ddl::snowflake_create_table::tuple_names;
use crate::parser::models::{Constraint, Parser};
use sqlquack_syntax::expressions::columns::models::{
AlterTableAction, ColumnDef, ConstraintModifiers, ForeignKeyRef, TableConstraint,
};
use sqlquack_syntax::expressions::tree::models::Expression;
use sqlquack_syntax::tokens::models::TokenType;
use sqlquack_syntax::tokens::types::Result;

impl Parser {
/// A column after ADD [ COLUMN ] [ IF NOT EXISTS ]: Snowflake's needs a type and takes the CREATE TABLE column options.
pub(crate) fn parse_added_column_def(&mut self) -> Result<ColumnDef> {
if !self.is_snowflake() {
return self.parse_column_def();
}
let column = self.parse_checked_table_column()?;
if column.no_type {
return Err(self.parse_error("Expected a data type"));
}
Ok(column)
}

/// Snowflake's `ADD <out-of-line constraint>`, or None (position unchanged) when no constraint follows ADD.
pub(crate) fn parse_snowflake_added_constraint(&mut self) -> Result<Option<AlterTableAction>> {
let starts = self.check(TokenType::Constraint)
|| self.check(TokenType::Unique)
|| self.check(TokenType::PrimaryKey)
|| self.check(TokenType::ForeignKey)
|| self.check(TokenType::Check);
if !self.is_snowflake() || !starts {
return Ok(None);
}
let constraint = self.parse_checked_constraint(false)?;
Ok(Some(AlterTableAction::AddConstraint(table_constraint(
constraint,
)?)))
}
}

/// An out-of-line constraint as the table constraint ALTER TABLE ADD holds, its property words kept in order.
fn table_constraint(constraint: Constraint) -> Result<TableConstraint> {
let Constraint {
kind,
name,
operand,
references,
properties,
} = constraint;
let has_constraint_keyword = name.is_some();
let modifiers = ConstraintModifiers {
properties,
..ConstraintModifiers::default()
};
Ok(match kind {
"UNIQUE" => TableConstraint::Unique {
name,
columns: tuple_names(operand),
key_parts: Vec::new(),
columns_parenthesized: true,
modifiers,
has_constraint_keyword,
nulls_not_distinct: false,
},
"PRIMARY KEY" => TableConstraint::PrimaryKey {
name,
columns: tuple_names(operand),
key_parts: Vec::new(),
timeseries_columns: Vec::new(),
include_columns: Vec::new(),
modifiers,
has_constraint_keyword,
},
"CHECK" => TableConstraint::Check {
name,
expression: operand.ok_or_else(|| {
sqlquack_syntax::tokens::errors::Error::internal(
"the parser reached a state its checks rule out",
)
})?,
modifiers,
},
_ => {
let (table, columns) = references.ok_or_else(|| {
sqlquack_syntax::tokens::errors::Error::internal(
"the parser reached a state its checks rule out",
)
})?;
let Expression::Table(table) = table else {
return Err(sqlquack_syntax::tokens::errors::Error::internal(
"the parser reached a state its checks rule out",
));
};
TableConstraint::ForeignKey {
name,
columns: tuple_names(operand),
references: Some(Box::new(ForeignKeyRef {
table: *table,
columns: tuple_names(columns),
on_delete: None,
on_update: None,
on_update_first: false,
match_type: None,
match_after_actions: false,
constraint_name: None,
deferrable: None,
has_foreign_key_keywords: true,
})),
on_delete: None,
on_update: None,
modifiers,
}
}
})
}
Loading
Loading