feat: support array arguments in snowflake scripts (#11702)

* feat: support array arguments in snowflake scripts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: cast the parsed snowflake list to array in the guidance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: parse a json array default of a sql list argument as a list

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-10-12 17:28:05 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent a92b29dae6
commit 14519884e4
7 changed files with 164 additions and 2 deletions
+44 -1
View File
@@ -235,7 +235,7 @@ lazy_static::lazy_static! {
// -- $name (type) = default
static ref RE_ARG_DUCKDB: Regex = Regex::new(r#"(?m)^-- \$(\w+) \(([A-Za-z0-9_\[\]]+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
static ref RE_ARG_SNOWFLAKE: Regex = Regex::new(r#"(?m)^-- \? (\w+) \((\w+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
static ref RE_ARG_SNOWFLAKE: Regex = Regex::new(r#"(?m)^-- \? (\w+) \((\w+(?:\[\])?)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
static ref RE_ARG_MSSQL: Regex = Regex::new(r#"(?m)^-- @(?:P|p)\d+ (\w+) \((\w+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
@@ -254,6 +254,12 @@ fn parsed_default(parsed_typ: &Typ, default: String) -> Option<serde_json::Value
Typ::Str(_) if default.len() >= 2 && default.starts_with("'") && default.ends_with("'") => {
Some(json!(&default[1..default.len() - 1]))
}
Typ::List(_) => Some(
serde_json::from_str::<serde_json::Value>(&default)
.ok()
.filter(|v| v.is_array())
.unwrap_or_else(|| json!(default)),
),
_ => Some(json!(default)),
}
}
@@ -1039,7 +1045,11 @@ pub fn parse_duckdb_typ(typ: &str) -> Typ {
}
pub fn parse_snowflake_typ(typ: &str) -> Typ {
if let Some(base_typ) = typ.strip_suffix("[]") {
return Typ::List(Box::new(parse_snowflake_typ(base_typ)));
}
match typ {
"array" => Typ::List(Box::new(Typ::Unknown)),
"varchar" | "text" | "string" | "char" | "character" => Typ::Str(None),
"binary" | "varbinary" => Typ::Bytes,
"date" | "time" | "datetime" | "timestamp" | "timestamp_ntz" | "timestamp_ltz"
@@ -1591,6 +1601,39 @@ SELECT ?;
Ok(())
}
#[test]
fn test_parse_snowflake_array_types() -> anyhow::Result<()> {
let code = r#"
-- ? groups (varchar[])
-- ? ids (int[]) = [1, 2]
-- ? anything (ARRAY)
SELECT 1;
"#;
let args = parse_snowflake_sig(code)?.args;
assert_eq!(args[1].default, Some(json!([1, 2])));
let got = args
.iter()
.map(|a| (a.name.as_str(), a.otyp.as_deref(), &a.typ))
.collect::<Vec<_>>();
assert_eq!(
got,
vec![
(
"groups",
Some("varchar[]"),
&Typ::List(Box::new(Typ::Str(None)))
),
("ids", Some("int[]"), &Typ::List(Box::new(Typ::Int))),
(
"anything",
Some("array"),
&Typ::List(Box::new(Typ::Unknown))
),
]
);
Ok(())
}
#[test]
fn test_parse_mssql_sig() -> anyhow::Result<()> {
let code = r#"
@@ -873,7 +873,8 @@ fn convert_typ_val(arg_t: String, arg_v: Value) -> Value {
let mut v = arg_v;
if !v.is_string() {
// if not string, convert to string for api request
// The SQL API has no ARRAY/VARIANT binding type: a list or object
// is bound as its JSON text, which the query reads with PARSE_JSON(?).
v = json!(v.to_string());
}
@@ -935,3 +936,16 @@ fn parse_val(value: &Value, typ: &str) -> Value {
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_arg_binds_as_json_text() {
assert_eq!(
convert_typ_val("varchar[]".to_string(), json!(["sales", "g/ops"])),
json!({ "type": "TEXT", "value": r#"["sales","g/ops"]"# })
);
}
}
+21
View File
@@ -5625,6 +5625,27 @@ Name the parameters by adding comments before the statement:
SELECT * FROM users WHERE name = ? AND age > ?;
\`\`\`
## Array parameters
Declare a list with a \`[]\` suffix on the element type (\`(varchar[])\`,
\`(int[])\`), or \`(array)\` for an untyped one. Snowflake cannot bind an array
directly, so Windmill binds the list as JSON text: wrap the placeholder with
\`PARSE_JSON(?)\`, and cast to \`ARRAY\` where a function expects one.
\`\`\`sql
-- ? groups (varchar[])
SELECT o.* FROM orders o
WHERE ARRAY_CONTAINS(o.owner_group::VARIANT, PARSE_JSON(?)::ARRAY);
\`\`\`
Or join on the elements with \`FLATTEN\`:
\`\`\`sql
-- ? groups (varchar[])
SELECT o.* FROM orders o
JOIN TABLE(FLATTEN(input => PARSE_JSON(?))) g ON g.value::STRING = o.owner_group;
\`\`\`
## Receiving an S3Object as a script parameter
Declare the arg with type \`(s3object)\`. Windmill renders an S3 file picker for
+21
View File
@@ -5815,6 +5815,27 @@ Name the parameters by adding comments before the statement:
SELECT * FROM users WHERE name = ? AND age > ?;
\`\`\`
## Array parameters
Declare a list with a \`[]\` suffix on the element type (\`(varchar[])\`,
\`(int[])\`), or \`(array)\` for an untyped one. Snowflake cannot bind an array
directly, so Windmill binds the list as JSON text: wrap the placeholder with
\`PARSE_JSON(?)\`, and cast to \`ARRAY\` where a function expects one.
\`\`\`sql
-- ? groups (varchar[])
SELECT o.* FROM orders o
WHERE ARRAY_CONTAINS(o.owner_group::VARIANT, PARSE_JSON(?)::ARRAY);
\`\`\`
Or join on the elements with \`FLATTEN\`:
\`\`\`sql
-- ? groups (varchar[])
SELECT o.* FROM orders o
JOIN TABLE(FLATTEN(input => PARSE_JSON(?))) g ON g.value::STRING = o.owner_group;
\`\`\`
## Receiving an S3Object as a script parameter
Declare the arg with type \`(s3object)\`. Windmill renders an S3 file picker for
+21
View File
@@ -1409,6 +1409,27 @@ Name the parameters by adding comments before the statement:
SELECT * FROM users WHERE name = ? AND age > ?;
```
## Array parameters
Declare a list with a `[]` suffix on the element type (`(varchar[])`,
`(int[])`), or `(array)` for an untyped one. Snowflake cannot bind an array
directly, so Windmill binds the list as JSON text: wrap the placeholder with
`PARSE_JSON(?)`, and cast to `ARRAY` where a function expects one.
```sql
-- ? groups (varchar[])
SELECT o.* FROM orders o
WHERE ARRAY_CONTAINS(o.owner_group::VARIANT, PARSE_JSON(?)::ARRAY);
```
Or join on the elements with `FLATTEN`:
```sql
-- ? groups (varchar[])
SELECT o.* FROM orders o
JOIN TABLE(FLATTEN(input => PARSE_JSON(?))) g ON g.value::STRING = o.owner_group;
```
## Receiving an S3Object as a script parameter
Declare the arg with type `(s3object)`. Windmill renders an S3 file picker for
@@ -84,6 +84,27 @@ Name the parameters by adding comments before the statement:
SELECT * FROM users WHERE name = ? AND age > ?;
```
## Array parameters
Declare a list with a `[]` suffix on the element type (`(varchar[])`,
`(int[])`), or `(array)` for an untyped one. Snowflake cannot bind an array
directly, so Windmill binds the list as JSON text: wrap the placeholder with
`PARSE_JSON(?)`, and cast to `ARRAY` where a function expects one.
```sql
-- ? groups (varchar[])
SELECT o.* FROM orders o
WHERE ARRAY_CONTAINS(o.owner_group::VARIANT, PARSE_JSON(?)::ARRAY);
```
Or join on the elements with `FLATTEN`:
```sql
-- ? groups (varchar[])
SELECT o.* FROM orders o
JOIN TABLE(FLATTEN(input => PARSE_JSON(?))) g ON g.value::STRING = o.owner_group;
```
## Receiving an S3Object as a script parameter
Declare the arg with type `(s3object)`. Windmill renders an S3 file picker for
+21
View File
@@ -10,6 +10,27 @@ Name the parameters by adding comments before the statement:
SELECT * FROM users WHERE name = ? AND age > ?;
```
## Array parameters
Declare a list with a `[]` suffix on the element type (`(varchar[])`,
`(int[])`), or `(array)` for an untyped one. Snowflake cannot bind an array
directly, so Windmill binds the list as JSON text: wrap the placeholder with
`PARSE_JSON(?)`, and cast to `ARRAY` where a function expects one.
```sql
-- ? groups (varchar[])
SELECT o.* FROM orders o
WHERE ARRAY_CONTAINS(o.owner_group::VARIANT, PARSE_JSON(?)::ARRAY);
```
Or join on the elements with `FLATTEN`:
```sql
-- ? groups (varchar[])
SELECT o.* FROM orders o
JOIN TABLE(FLATTEN(input => PARSE_JSON(?))) g ON g.value::STRING = o.owner_group;
```
## Receiving an S3Object as a script parameter
Declare the arg with type `(s3object)`. Windmill renders an S3 file picker for