Skip to content
Closed
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions coverage/syntax/unobservable/functions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -506,3 +506,39 @@ cases = [
]
runs = []
probes = []

[[unobservable]]
obligation = "sf.functions.system-disable-preview-access.o001"
form = "sf.functions.system-disable-preview-access"
construct = "the documented form with every required part and no optional part: SELECT SYSTEM$DISABLE_PREVIEW_ACCESS ( ) ;"
reason = "not submitted: held by the capture guard in packet brq-execute-00 under its account-state rule (it changes account-wide preview feature access)"
cause = "not_submitted"
cases = [
{ case_id = "sf.functions.system-disable-preview-access.k1fad69043462", sql_sha256 = "1fad690434621ade8731ed5e4bca6d9cc7ab2265928df74af46c5bc01039afdb" },
]
runs = []
probes = []

[[unobservable]]
obligation = "sf.functions.system-enable-preview-access.o001"
form = "sf.functions.system-enable-preview-access"
construct = "the documented form with every required part and no optional part: SELECT SYSTEM$ENABLE_PREVIEW_ACCESS ( ) ;"
reason = "not submitted: held by the capture guard in packet brq-execute-00 under its account-state rule (it changes account-wide preview feature access)"
cause = "not_submitted"
cases = [
{ case_id = "sf.functions.system-enable-preview-access.k24490f3e8782", sql_sha256 = "24490f3e8782143d077daf9758752bc75a6937b642fa3e041f72ed2e26d4dda9" },
]
runs = []
probes = []

[[unobservable]]
obligation = "sf.functions.system-enforce-privatelink-access-only.o001"
form = "sf.functions.system-enforce-privatelink-access-only"
construct = "the documented form with every required part and no optional part: SELECT SYSTEM$ENFORCE_PRIVATELINK_ACCESS_ONLY ( ) ;"
reason = "not submitted: held by the capture guard in packet brq-execute-00 under its account-state rule (it blocks public access to the account)"
cause = "not_submitted"
cases = [
{ case_id = "sf.functions.system-enforce-privatelink-access-only.kfff6ddce6695", sql_sha256 = "fff6ddce6695aaa8e8b7c143f7eea607173ee1e9d1d3420c9ea061dcba17179e" },
]
runs = []
probes = []
5 changes: 4 additions & 1 deletion crates/sqlquack-core/src/classification/_helpers/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ fn function_construct(name: &str) -> Construct {
{
return Construct::new(ConstructFamily::Function, PROJECT_FUNCTION_CONSTRUCT);
}
Construct::new(ConstructFamily::Function, name.to_ascii_uppercase())
Construct::new(
ConstructFamily::Function,
name.to_ascii_uppercase().replace(' ', "_"),
)
}

fn cast_constructs(variant: &str, cast: &Cast, dialect: SourceDialect) -> Vec<Construct> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ fn given_snowflake_query_when_classifying_then_reports_each_construct_family() {
sql: "SELECT zeroifnull(o.amount) AS amount FROM orders o",
expected_keys: &["fn:ZEROIFNULL", "node:alias", "node:column", "node:select"],
},
ConstructTestCase {
description: "a multi-word call such as GROUPING SETS keys with underscores, never a space",
sql: "SELECT a, b, SUM(c) FROM t GROUP BY GROUPING SETS ((a), (b))",
expected_keys: &["fn:GROUPING_SETS"],
},
ConstructTestCase {
description: "casts record the node, the base type and the `::` spelling",
sql: "SELECT o.amount::NUMBER(10, 2) AS amount FROM orders o",
Expand Down
1 change: 1 addition & 0 deletions crates/sqlquack-syntax-coverage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ clap.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
sqlquack-core.workspace = true
sqlquack-syntax.workspace = true
toml.workspace = true

Expand Down
14 changes: 11 additions & 3 deletions crates/sqlquack-syntax-coverage/src/cli/_helpers/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

use std::process::Command;

use sqlquack_syntax::{CallMode, Dialect, Expression, ExpressionWalk, ParseOptions};
use sqlquack_core::parsing::constants::MAX_FUNCTION_CALL_DEPTH;
use sqlquack_syntax::{
CallMode, ComplexityGuardOptions, Dialect, Expression, ExpressionWalk, ParseOptions,
};

use crate::check::models::{LocalFailure, LocalVerdict};
use crate::formats::main::content_sha256::content_sha256;
Expand All @@ -12,12 +15,17 @@ const PARSER_PATH: &str = "crates/sqlquack-syntax";
/// The code prefix of every complexity-guard error, which is never a syntax verdict.
const GUARD_PREFIX: &str = "E_GUARD_";

/// SQLQuack's Snowflake verdict; a Command or Raw node anywhere makes the parse opaque.
/// SQLQuack's Snowflake verdict under the product's call-depth guard; a Command or Raw node makes it opaque.
pub(crate) fn snowflake_verdict(sql: &str) -> LocalVerdict {
match sqlquack_syntax::parse_statements_with(
sql,
Dialect::Snowflake,
&ParseOptions::default(),
&ParseOptions {
complexity_guard: Some(ComplexityGuardOptions {
max_function_call_depth: Some(MAX_FUNCTION_CALL_DEPTH),
..ComplexityGuardOptions::default()
}),
},
CallMode::Faithful,
) {
Ok(statements) => LocalVerdict::Accepted {
Expand Down
3 changes: 3 additions & 0 deletions crates/sqlquack-syntax-coverage/src/results/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pub struct RecordedStatement {
pub refused: Vec<String>,
/// Every construct the statement uses, sorted.
pub constructs: Vec<String>,
/// Why SQLQuack's parser refused the statement; its constructs are then only token spellings.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unparsed: Option<String>,
}

/// What one statement shows; only the first two are results matching Snowflake.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ pub(crate) fn statement(
codes: vec!["SQ0003".to_owned()],
refused: Vec::new(),
constructs: with_call_shapes(constructs),
unparsed: None,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ impl Parser {
let name = self.parse_object_name()?;
let mut operands: Vec<Expression> = Vec::new();
let mut properties: Vec<ObjectProperty> = Vec::new();
if matches!(kind, "EXPERIMENT")
&& (self.check(TokenType::Type) || self.check_identifier("TYPE"))
{
self.skip();
self.expect_token(TokenType::Eq)?;
properties.push(property("TYPE", None, Some(self.parse_object_value()?)));
}
let source = match kind {
"MODEL" => {
if self.match_token(TokenType::With) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ impl Parser {

match behavior {
crate::functions::dispatch::models::ParserDispatchBehavior::ExprListFunction => {
let args = self.parse_expression_list()?;
let args = if self.applies_snowflake_context_rules() && self.check(TokenType::RParen)
{
Vec::new()
} else {
self.parse_expression_list()?
};
self.expect_token(TokenType::RParen)?;
Ok(Some(make_unquoted_function(name, args)))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ impl Parser {
{
if self.is_snowflake()
&& self.tokens.get(self.current + 1).is_some_and(|t| {
["CLONE", "FROM", "AS", "USING", "LIKE", "("]
["CLONE", "FROM", "AS", "USING", "LIKE", "(", "TYPE"]
.iter()
.any(|word| t.text.eq_ignore_ascii_case(word))
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ impl Parser {
}
}

/// Refuses PRECISION or VARYING alone as a type, which Snowflake reads only after DOUBLE or CHARACTER.
pub(crate) fn reject_snowflake_lone_type_word(&self, name: &str) -> Result<()> {
match self.applies_snowflake_context_rules() && matches!(name, "PRECISION" | "VARYING") {
true => Err(self.parse_error(format!(
"{name} is not a Snowflake type on its own; it only follows DOUBLE or CHARACTER"
))),
false => Ok(()),
}
}

/// Refuses TABLE( ) or IDENTIFIER( ) around nothing or a bare name, except a named argument's TABLE(name).
pub(crate) fn reject_snowflake_name_wrappers(&self, statements: &[Expression]) -> Result<()> {
use crate::traversal::types::ExpressionWalk;
Expand Down
4 changes: 4 additions & 0 deletions crates/sqlquack-syntax/src/parser/_helpers/types/casts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,13 +382,17 @@ impl Parser {
let (subtype, srid) = self.parse_spatial_type_args()?;
return Ok(DataType::Geometry { subtype, srid });
}
let is_quoted = self.check(TokenType::QuotedIdentifier);
let mut raw_name = self.expect_identifier_or_keyword()?;
while self.match_token(TokenType::Dot) {
let part = self.expect_identifier_or_keyword()?;
raw_name.push('.');
raw_name.push_str(&part);
}
let mut name = raw_name.to_ascii_uppercase();
if !is_quoted {
self.reject_snowflake_lone_type_word(&name)?;
}

if name == NATIONAL {
let next_upper = if !self.is_at_end() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ impl Parser {
});
}
let name = raw_name.to_ascii_uppercase();
self.reject_snowflake_lone_type_word(&name)?;

let base_type = match name.as_str() {
name if DataType::from_unsigned_name(name).is_some() => {
Expand Down
94 changes: 93 additions & 1 deletion crates/sqlquack-syntax/tests/parser/test_snowflake_fixes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,32 @@ fn given_a_snowflake_create_model_gateway_or_experiment_when_parsing_then_its_so
SnowflakeVerdictTestCase {
description: "keeps CREATE EXPERIMENT v1 TYPE = 'AI_FUNCTION_EVALUATION' FROM SPECIFICATIO",
sql: "CREATE EXPERIMENT v1 TYPE = 'AI_FUNCTION_EVALUATION' FROM SPECIFICATION $$v1 $$ ;",
expected_kind: Some("raw"),
expected_kind: Some("create_object"),
},
SnowflakeVerdictTestCase {
description: "keeps an experiment TYPE without a specification (recorded as parsed)",
sql: "CREATE EXPERIMENT v1 TYPE = m1 ;",
expected_kind: Some("create_object"),
},
SnowflakeVerdictTestCase {
description: "an experiment specification without FROM (recorded errno 1003)",
sql: "CREATE EXPERIMENT v1 TYPE = 'AI_FUNCTION_EVALUATION' SPECIFICATION $$v1 $$ ;",
expected_kind: None,
},
SnowflakeVerdictTestCase {
description: "an experiment FROM without SPECIFICATION (recorded errno 1003)",
sql: "CREATE EXPERIMENT v1 TYPE = 'AI_FUNCTION_OPTIMIZATION' FROM $$v1 $$ ;",
expected_kind: None,
},
SnowflakeVerdictTestCase {
description: "an experiment TYPE with no value (recorded errno 1003)",
sql: "CREATE EXPERIMENT v1 TYPE = ;",
expected_kind: None,
},
SnowflakeVerdictTestCase {
description: "a name and TYPE with no object kind (recorded errno 1003)",
sql: "CREATE v1 TYPE = 'AI_FUNCTION_EVALUATION' FROM SPECIFICATION $$v1 $$ ;",
expected_kind: None,
},
SnowflakeVerdictTestCase {
description: "keeps ALTER TABLE v1 MODIFY DATA METRIC FUNCTION v1 ON ( v1 ) SUSPEND ;",
Expand Down Expand Up @@ -1253,3 +1278,70 @@ fn given_snowflake_secret_resource_monitor_and_integration_alters_when_parsing_t
);
}
}

#[test]
fn given_a_snowflake_cast_or_conversion_call_when_parsing_then_the_recorded_verdicts_hold() {
let test_cases = [
SnowflakeVerdictTestCase {
description: "PRECISION alone as a type (recorded errno 1003)",
sql: "SELECT CAST ( NULL AS PRECISION ) ;",
expected_kind: None,
},
SnowflakeVerdictTestCase {
description: "VARYING alone as a type (recorded errno 1003)",
sql: "SELECT CAST ( NULL AS VARYING ) ;",
expected_kind: None,
},
SnowflakeVerdictTestCase {
description: "keeps DOUBLE PRECISION",
sql: "SELECT CAST ( NULL AS DOUBLE PRECISION ) ;",
expected_kind: Some("select"),
},
SnowflakeVerdictTestCase {
description: "keeps CHARACTER VARYING",
sql: "SELECT CAST ( NULL AS CHARACTER VARYING ) ;",
expected_kind: Some("select"),
},
SnowflakeVerdictTestCase {
description: "keeps an unknown type name, a compile error (recorded errno 2040)",
sql: "SELECT CAST ( NULL AS FOOBAR ) ;",
expected_kind: Some("select"),
},
SnowflakeVerdictTestCase {
description:
"keeps TO_TIMESTAMP with no arguments, a compile error (recorded as parsed)",
sql: "SELECT TO_TIMESTAMP ( ) ;",
expected_kind: Some("select"),
},
SnowflakeVerdictTestCase {
description:
"keeps TRY_TO_TIMESTAMP with no arguments, a compile error (recorded as parsed)",
sql: "SELECT TRY_TO_TIMESTAMP ( ) ;",
expected_kind: Some("select"),
},
SnowflakeVerdictTestCase {
description: "!SPCS_CANCEL_JOB with no instance, kept refused like == (recorded errno 2139, D213)",
sql: "SELECT !SPCS_CANCEL_JOB ( ) ;",
expected_kind: None,
},
SnowflakeVerdictTestCase {
description: "!SPCS_WAIT_FOR with no instance, kept refused like == (recorded as parsed, errno 904, D213)",
sql: "SELECT !SPCS_WAIT_FOR ( v1 , v1 ) ;",
expected_kind: None,
},
SnowflakeVerdictTestCase {
description: "IDENTIFIER with no arguments (recorded errno 1003)",
sql: "SELECT * FROM IDENTIFIER ( ) ;",
expected_kind: None,
},
];

for test_case in test_cases {
assert_eq!(
snowflake_kind(test_case.sql),
test_case.expected_kind.map(str::to_owned),
"{}",
test_case.description
);
}
}
19 changes: 10 additions & 9 deletions crates/sqlquack/src/statements/_helpers/constructs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,23 @@ pub(crate) fn project_key(key: String, own: &BTreeSet<String>) -> String {
}
}

/// Every construct and spelling key the case's statement uses, sorted, its own functions as one project key.
pub(crate) fn constructs(case: &LoadedCase, own: &BTreeSet<String>) -> Result<Vec<String>, String> {
/// Every construct and spelling key the case's statement uses, sorted, and why it did not parse, if not.
pub(crate) fn constructs(
case: &LoadedCase,
own: &BTreeSet<String>,
) -> (Vec<String>, Option<String>) {
let sql = &case.source_sql;
let lines = SourceLines::new(sql);
let statements = parse_source(sql, SourceDialect::Snowflake).map_err(|failure| {
format!(
"{}: a recorded statement does not parse: {failure:?}",
case.reference
)
})?;
let mut keys: BTreeSet<String> = token_spellings(sql);
let statements = match parse_source(sql, SourceDialect::Snowflake) {
Ok(statements) => statements,
Err(failure) => return (keys.into_iter().collect(), Some(failure.to_string())),
};
for statement in &statements {
keys.extend(tree_spellings(statement));
for occurrence in classify(statement, &lines, SourceDialect::Snowflake) {
keys.insert(project_key(occurrence.construct.key(), own));
}
}
Ok(keys.into_iter().collect())
(keys.into_iter().collect(), None)
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub fn write_translation_statements(
};
let (outcome, codes, refused) = outcome(case, verdict, converter);
let own = own_functions(case);
let (constructs, unparsed) = constructs(case, &own);
statements.push(Statement {
reference: verdict.reference.clone(),
hash: verdict.hash.clone(),
Expand All @@ -39,7 +40,8 @@ pub fn write_translation_statements(
.into_iter()
.map(|key| project_key(key, &own))
.collect(),
constructs: constructs(case, &own)?,
constructs,
unparsed,
});
}
let count = statements.len();
Expand Down
3 changes: 3 additions & 0 deletions crates/sqlquack/src/statements/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ pub struct Statement {
pub refused: Vec<String>,
/// Every construct the Snowflake statement uses, which links it to catalogue forms.
pub constructs: Vec<String>,
/// Why SQLQuack's parser refused the statement; its constructs are then only token spellings.
#[serde(skip_serializing_if = "Option::is_none")]
pub unparsed: Option<String>,
}

/// The whole file `sqlquack verify --translation-statements` writes.
Expand Down
Loading
Loading