Skip to content

Commit 31f40e8

Browse files
hovaescoclaude
andauthored
Task LAV-1720: EXTERNAL_ACCESS_INTEGRATIONS / SECRETS on CREATE FUNCTION and CREATE PROCEDURE, incl. the SHOW and INFORMATION_SCHEMA columns (apache#2254)
* task LAV-1720: WIP — commit stranded agent work * Task LAV-1720: create-time EAI/SECRETS validation + read-surface wiring (WIP) Implements the EXTERNAL_ACCESS_INTEGRATIONS / SECRETS create-time validator (__snowflake$validate_udf_external_access), XSFD5-8 error mappings, GET_DDL separator fix, and wires validation into the function + Python-procedure create paths. Blocked on the SHOW USER FUNCTIONS is_ai_function shared-shape decision (see Linear). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Task LAV-1720: EXTERNAL_ACCESS_INTEGRATIONS / SECRETS on CREATE FUNCTION/PROCEDURE Parse both clauses on CREATE FUNCTION and (via the PROCEDURE->FUNCTION rewrite) CREATE PROCEDURE, store them structured, validate at create time against live integrations/secrets with real-Snowflake errors, and project them on SHOW, INFORMATION_SCHEMA and GET_DDL. Real-Snowflake finding: a SECRETS clause with no EXTERNAL_ACCESS_INTEGRATIONS is accepted (there is no integration to gate it against), so the allow check only fires when integrations are referenced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Task LAV-1720: validate EAI/SECRETS on LANGUAGE JAVA create path --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5e6fcdd commit 31f40e8

2 files changed

Lines changed: 58 additions & 3 deletions

File tree

src/parser/mod.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6449,14 +6449,20 @@ impl<'a> Parser<'a> {
64496449
}
64506450

64516451
/// True when the next token is a Snowflake Python-UDF property name
6452-
/// (`RUNTIME_VERSION`, `HANDLER`, `IMPORTS`, `PACKAGES`). None of these are
6453-
/// reserved keywords, so they are recognised by their bare-word spelling.
6452+
/// (`RUNTIME_VERSION`, `HANDLER`, `IMPORTS`, `PACKAGES`,
6453+
/// `EXTERNAL_ACCESS_INTEGRATIONS`, `SECRETS`). None of these are reserved
6454+
/// keywords, so they are recognised by their bare-word spelling.
64546455
fn peek_snowflake_function_property(&self) -> bool {
64556456
matches!(&self.peek_token_ref().token, Token::Word(w)
64566457
if w.quote_style.is_none()
64576458
&& matches!(
64586459
w.value.to_ascii_uppercase().as_str(),
6459-
"RUNTIME_VERSION" | "HANDLER" | "IMPORTS" | "PACKAGES"
6460+
"RUNTIME_VERSION"
6461+
| "HANDLER"
6462+
| "IMPORTS"
6463+
| "PACKAGES"
6464+
| "EXTERNAL_ACCESS_INTEGRATIONS"
6465+
| "SECRETS"
64606466
))
64616467
}
64626468

tests/sqlparser_snowflake.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6157,6 +6157,55 @@ fn test_select_from_stage_comma_join_lateral() {
61576157
snowflake().verified_stmt("SELECT * FROM @stage/test.jsonl, other_table");
61586158
}
61596159

6160+
/// `CREATE FUNCTION … EXTERNAL_ACCESS_INTEGRATIONS = (…) SECRETS = ('a' = s)`
6161+
/// parses both Snowflake property clauses into `options` as key-value pairs:
6162+
/// the integrations list as a tuple of identifiers, the secrets list as a tuple
6163+
/// of `'alias' = <secret>` bindings.
6164+
#[test]
6165+
fn test_snowflake_create_function_external_access_and_secrets() {
6166+
let sql = "CREATE FUNCTION f(x NUMBER) RETURNS NUMBER LANGUAGE PYTHON \
6167+
RUNTIME_VERSION = '3.10' HANDLER = 'run' \
6168+
EXTERNAL_ACCESS_INTEGRATIONS = (eai_a, eai_b) \
6169+
SECRETS = ('cred' = my_secret) AS 'def run(x): return x'";
6170+
let options = match snowflake().parse_sql_statements(sql).unwrap().remove(0) {
6171+
Statement::CreateFunction(cf) => cf.options.expect("options should be present"),
6172+
other => panic!("expected CreateFunction, got {other:?}"),
6173+
};
6174+
6175+
let eai = options
6176+
.iter()
6177+
.find_map(|o| match o {
6178+
SqlOption::KeyValue { key, value }
6179+
if key.value.eq_ignore_ascii_case("EXTERNAL_ACCESS_INTEGRATIONS") =>
6180+
{
6181+
Some(value)
6182+
}
6183+
_ => None,
6184+
})
6185+
.expect("EXTERNAL_ACCESS_INTEGRATIONS option");
6186+
match eai {
6187+
Expr::Tuple(items) => assert_eq!(items.len(), 2),
6188+
other => panic!("expected a tuple of integrations, got {other:?}"),
6189+
}
6190+
6191+
let secrets = options
6192+
.iter()
6193+
.find_map(|o| match o {
6194+
SqlOption::KeyValue { key, value } if key.value.eq_ignore_ascii_case("SECRETS") => {
6195+
Some(value)
6196+
}
6197+
_ => None,
6198+
})
6199+
.expect("SECRETS option");
6200+
match secrets {
6201+
Expr::Tuple(items) => match &items[0] {
6202+
Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOperator::Eq),
6203+
other => panic!("expected 'alias' = secret binding, got {other:?}"),
6204+
},
6205+
other => panic!("expected a tuple of secret bindings, got {other:?}"),
6206+
}
6207+
}
6208+
61606209
/// Bare assignment `var := expr` inside `BEGIN...END` scripting blocks.
61616210
#[test]
61626211
fn test_scripting_bare_assignment() {

0 commit comments

Comments
 (0)