Skip to content
Merged
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
40 changes: 40 additions & 0 deletions corpora/polyglot-check/expected_differences.toml
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,41 @@ dialects = ["Snowflake"]
sqlquack_error = "Snowflake allows BY NAME only after UNION, without an ON column list"
note = "BY NAME ON (...) after EXCEPT is BigQuery's; Snowflake takes BY NAME only after UNION (recorded errno 1003)"

[[rejection]]
id = "snowflake_function_body_number"
sql = "CREATE FUNCTION f(x SMALLDECIMAL ARRAY) RETURNS INT AS 1"
dialects = ["Snowflake"]
sqlquack_error = "Expected the function body as a string"
note = "Snowflake reads a function body only as a string (recorded errno 1003 for CREATE FUNCTION v1 ( ) RETURNS INT AS m1)"

[[rejection]]
id = "snowflake_function_body_return"
sql = "CREATE FUNCTION a.b(x INT) RETURNS INT AS RETURN x + 1"
dialects = ["Snowflake"]
sqlquack_error = "Expected the function body as a string"
note = "Snowflake has no RETURN body; a function body is a string or $$ block (recorded errno 1003 for CREATE FUNCTION v1 ( ) RETURNS INT AS m1)"

[[rejection]]
id = "snowflake_function_body_return_contains_sql"
sql = "CREATE FUNCTION a.b(x TEXT) RETURNS TEXT CONTAINS SQL AS RETURN x"
dialects = ["Snowflake"]
sqlquack_error = "Expected the function body as a string"
note = "Snowflake has no RETURN body; a function body is a string or $$ block (recorded errno 1003 for CREATE FUNCTION v1 ( ) RETURNS INT AS m1)"

[[rejection]]
id = "snowflake_function_body_return_modifies_sql"
sql = "CREATE FUNCTION a.b(x TEXT) RETURNS TEXT LANGUAGE SQL MODIFIES SQL DATA AS RETURN x"
dialects = ["Snowflake"]
sqlquack_error = "Expected the function body as a string"
note = "Snowflake has no RETURN body; a function body is a string or $$ block (recorded errno 1003 for CREATE FUNCTION v1 ( ) RETURNS INT AS m1)"

[[rejection]]
id = "snowflake_function_body_return_reads_sql"
sql = "CREATE FUNCTION a.b(x TEXT) LANGUAGE SQL READS SQL DATA RETURNS TEXT AS RETURN x"
dialects = ["Snowflake"]
sqlquack_error = "Expected the function body as a string"
note = "Snowflake has no RETURN body; a function body is a string or $$ block (recorded errno 1003 for CREATE FUNCTION v1 ( ) RETURNS INT AS m1)"

[[rejection]]
id = "snowflake_string_part_extract"
sql = "SELECT EXTRACT('{quoted}' FROM created_at) FROM events"
Expand Down Expand Up @@ -1049,6 +1084,11 @@ id = "star_exclude_quoted_name"
marker = "EXCLUDE(\""
note = "SELECT * EXCLUDE (\"note\") excludes the case-sensitive column note, not NOTE; polyglot drops the quotes of a star's EXCLUDE names"

[[kept_quoting]]
id = "comment_on_quoted_name"
marker = "COMMENTON"
note = "COMMENT ON TABLE \"v1\" names the case-sensitive object v1, not V1; polyglot drops the quotes of a COMMENT ON name; crates/sqlquack-syntax/tests/parser/test_fixes.rs pins the tree"

# Wrapper nodes SQLQuack keeps where polyglot drops them. The check counts an entry only when the
# statement, without whitespace and ignoring case, contains `marker`, and both trees are equal once
# every `{ "<node>": { "this": … } }` in SQLQuack's tree is replaced by its operand.
Expand Down
5 changes: 5 additions & 0 deletions crates/sqlquack-polyglot-check/tests/runs/test_exact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fn given_committed_corpora_when_parsed_by_both_parsers_then_results_are_identica
("snowflake_cte_without_as_prose".to_owned(), 1),
("snowflake_reserved_type_name".to_owned(), 1),
("snowflake_string_part_extract".to_owned(), 1),
("snowflake_function_body_number".to_owned(), 1),
("snowflake_collate_needs_string".to_owned(), 1),
("snowflake_dot_after_literal".to_owned(), 1),
("snowflake_by_name_on_except".to_owned(), 1),
Expand Down Expand Up @@ -63,6 +64,10 @@ fn given_committed_corpora_when_parsed_by_both_parsers_then_results_are_identica
expected_outcome: ExactOutcome {
loaded: true,
rejected: BTreeMap::from([
("snowflake_function_body_return".to_owned(), 1),
("snowflake_function_body_return_contains_sql".to_owned(), 1),
("snowflake_function_body_return_modifies_sql".to_owned(), 1),
("snowflake_function_body_return_reads_sql".to_owned(), 1),
("snowflake_cte_without_as".to_owned(), 1),
("snowflake_empty_call_argument".to_owned(), 1),
("snowflake_reserved_cte_name".to_owned(), 1),
Expand Down
36 changes: 36 additions & 0 deletions crates/sqlquack-syntax/src/expressions/schema_objects/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,9 @@ pub struct CreateType {
pub name: TableRef,
pub definition: TypeDefinition,
pub if_not_exists: bool,
/// OR REPLACE clause
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub or_replace: bool,
}

/// Type definition
Expand All @@ -1068,6 +1071,11 @@ pub enum TypeDefinition {
output: String,
internallength: Option<i32>,
},
/// Snowflake user-defined type: CREATE TYPE name AS data_type [COMMENT = 'text']
Alias {
data_type: DataType,
comment: Option<String>,
},
/// Domain type
Domain {
base_type: DataType,
Expand Down Expand Up @@ -1097,6 +1105,7 @@ impl CreateType {
name: TableRef::new(name),
definition: TypeDefinition::Enum(values),
if_not_exists: false,
or_replace: false,
}
}

Expand All @@ -1105,6 +1114,7 @@ impl CreateType {
name: TableRef::new(name),
definition: TypeDefinition::Composite(attributes),
if_not_exists: false,
or_replace: false,
}
}
}
Expand Down Expand Up @@ -1150,6 +1160,32 @@ pub struct AlterObject {
pub properties: Vec<ObjectProperty>,
}

/// Snowflake CREATE of an object from another source, e.g. `CREATE DATABASE d1 FROM SHARE a1.s1`.
#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CreateObject {
/// The object kind, e.g. "DATABASE", "SCHEMA", "TABLE", "DYNAMIC TABLE"
pub kind: String,
/// A modifier before the kind, e.g. "TRANSIENT"
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modifier: Option<String>,
/// OR REPLACE clause
#[serde(default)]
pub or_replace: bool,
/// IF NOT EXISTS clause
#[serde(default)]
pub if_not_exists: bool,
/// The object name
pub name: TableRef,
/// The source's keywords: "BACKUP SET", "SHARE", "REPLICA" or "ARCHIVE"
pub source: String,
/// The source's operands in order: names, an identifier string or a condition
#[serde(default)]
pub operands: Vec<Expression>,
/// The properties set after the source
#[serde(default)]
pub properties: Vec<ObjectProperty>,
}

/// One property of a Snowflake ALTER action, e.g. `SCHEDULE = '1 MINUTE'` or `TAG t1 = 'v'`.
#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ObjectProperty {
Expand Down
7 changes: 5 additions & 2 deletions crates/sqlquack-syntax/src/expressions/statements/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,11 @@ pub struct CreateTask {
pub name: String,
/// Raw text of properties between name and AS (WAREHOUSE, SCHEDULE, etc.)
pub properties: String,
/// The SQL statement body after AS
pub body: Expression,
/// The SQL statement body after AS; absent for `CLONE source`
pub body: Option<Expression>,
/// Source task of `CREATE TASK name CLONE source`
#[serde(default, skip_serializing_if = "Option::is_none")]
pub clone_source: Option<TableRef>,
}

/// Raw/unparsed SQL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ impl Expression {
| Expression::DropView(_)
| Expression::AlterView(_)
| Expression::AlterObject(_)
| Expression::CreateObject(_)
| Expression::AlterIndex(_)
| Expression::Truncate(_)
| Expression::TruncateTable(_)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ impl Expression {
Expression::DropView(_) => "drop_view",
Expression::AlterView(_) => "alter_view",
Expression::AlterObject(_) => "alter_object",
Expression::CreateObject(_) => "create_object",
Expression::AlterIndex(_) => "alter_index",
Expression::Truncate(_) => "truncate",
Expression::Use(_) => "use",
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlquack-syntax/src/expressions/tree/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ use crate::expressions::schema_objects::models::AlterSequence;
use crate::expressions::schema_objects::models::AlterView;
use crate::expressions::schema_objects::models::CreateDatabase;
use crate::expressions::schema_objects::models::CreateFunction;
use crate::expressions::schema_objects::models::CreateObject;
use crate::expressions::schema_objects::models::CreateProcedure;
use crate::expressions::schema_objects::models::CreateSchema;
use crate::expressions::schema_objects::models::CreateSequence;
Expand Down Expand Up @@ -1180,6 +1181,7 @@ pub enum Expression {
DropView(Box<DropView>),
AlterView(Box<AlterView>),
AlterObject(Box<AlterObject>),
CreateObject(Box<CreateObject>),
AlterIndex(Box<AlterIndex>),
Truncate(Box<Truncate>),
Use(Box<Use>),
Expand Down
20 changes: 16 additions & 4 deletions crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1156,11 +1156,11 @@ impl Parser {

/// Parse a qualified name (schema.table.column or just table)
pub(crate) fn parse_qualified_name(&mut self) -> Result<Expression> {
let first = self.parse_unquoted_identifier(Self::expect_identifier_or_keyword)?;
let first = self.parse_quoting_identifier(Self::expect_identifier_or_keyword)?;
let mut parts = vec![first];

while self.match_token(TokenType::Dot) {
let next = self.parse_unquoted_identifier(Self::expect_identifier_or_keyword)?;
let next = self.parse_quoting_identifier(Self::expect_identifier_or_keyword)?;
parts.push(next);
}

Expand All @@ -1186,7 +1186,10 @@ impl Parser {
.map(|(start, end)| start.through(end));
let table_name = parts
.iter()
.map(|id| id.name.as_str())
.map(|id| match id.quoted {
true => format!("\"{}\"", id.name.replace('"', "\"\"")),
false => id.name.clone(),
})
.collect::<Vec<_>>()
.join(".");
let mut table = Identifier::new(table_name);
Expand Down Expand Up @@ -1492,7 +1495,10 @@ impl Parser {

let if_not_exists =
self.match_keywords(&[TokenType::If, TokenType::Not, TokenType::Exists]);
let name = self.parse_table_ref()?;
let name = match self.is_snowflake() {
true => self.parse_object_name()?,
false => self.parse_table_ref()?,
};

let (parameters, has_parens) = if self.match_token(TokenType::LParen) {
let params = self.parse_function_parameters()?;
Expand Down Expand Up @@ -1731,6 +1737,12 @@ impl Parser {
mut function: CreateFunction,
) -> Result<CreateFunction> {
function = record_function_property(function, FunctionPropertyKind::As);
if self.is_snowflake()
&& !self.check(TokenType::String)
&& !self.check(TokenType::DollarString)
{
return Err(self.parse_error("Expected the function body as a string"));
}
if self.match_identifier("RETURN") {
let expr = if self.check(TokenType::Select) || self.check(TokenType::With) {
self.parse_statement()?
Expand Down
46 changes: 39 additions & 7 deletions crates/sqlquack-syntax/src/parser/_helpers/ddl/create_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ impl Parser {
)
);

let name = self.parse_table_ref()?;
let name = match self.is_snowflake() {
true => self.parse_object_name()?,
false => self.parse_table_ref()?,
};

let uuid = { None };

Expand Down Expand Up @@ -162,11 +165,7 @@ impl Parser {

let copy_grants = self.match_text_seq(&["COPY", "GRANTS"]);

let using_template = if self.match_text_seq(&["USING", "TEMPLATE"]) {
Some(Box::new(self.parse_primary()?))
} else {
None
};
let using_template = self.parse_using_template()?;

if using_template.is_some() {
return self.parse_create_table_using_template(
Expand Down Expand Up @@ -220,7 +219,8 @@ impl Parser {
);
}

if !self.check(TokenType::LParen) && is_special_modifier {
let snowflake_hybrid = self.check_snowflake_hybrid_body(table_modifier)?;
if !self.check(TokenType::LParen) && is_special_modifier && !snowflake_hybrid {
return self.parse_create_table_special_without_columns(
CreateTableHead {
name,
Expand Down Expand Up @@ -1539,3 +1539,35 @@ impl Parser {
Ok(options)
}
}

impl Parser {
/// Parses `USING TEMPLATE`; Snowflake takes only a string or a parenthesized query.
fn parse_using_template(&mut self) -> Result<Option<Box<Expression>>> {
if !self.match_text_seq(&["USING", "TEMPLATE"]) {
return Ok(None);
}
let query = self.check(TokenType::String)
|| self.check(TokenType::LParen)
&& matches!(
self.peek_nth(1).map(|token| token.token_type),
Some(TokenType::Select | TokenType::With)
);
if self.is_snowflake() && !query {
return Err(
self.parse_error("Expected a string or a parenthesized query after USING TEMPLATE")
);
}
Ok(Some(Box::new(self.parse_primary()?)))
}

/// Reports whether this is a Snowflake hybrid table, which needs columns, AS or LIKE.
fn check_snowflake_hybrid_body(&mut self, table_modifier: Option<&str>) -> Result<bool> {
let hybrid = self.is_snowflake() && matches!(table_modifier, Some("HYBRID"));
if hybrid && !self.check(TokenType::LParen) && !self.check(TokenType::Like) {
return Err(
self.parse_error("Expected columns, AS or LIKE after the hybrid table name")
);
}
Ok(hybrid)
}
}
1 change: 1 addition & 0 deletions crates/sqlquack-syntax/src/parser/_helpers/ddl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub(crate) mod snowflake_alter_objects;
pub(crate) mod snowflake_alter_resources;
pub(crate) mod snowflake_alter_tables;
pub(crate) mod snowflake_commands;
pub(crate) mod snowflake_create_from;
pub(crate) mod snowflake_show;
pub(crate) mod snowflake_statements;
pub(crate) mod table_options;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ impl Parser {
}

/// Parse an object name: an identifier, never a string or an unquoted reserved keyword.
pub(super) fn parse_object_name(&mut self) -> Result<TableRef> {
pub(crate) fn parse_object_name(&mut self) -> Result<TableRef> {
if self.check(TokenType::String)
|| self.is_snowflake_reserved_keyword()
|| !self.is_object_name_start()
Expand Down
Loading