diff --git a/book/src/language-features/s-strings.md b/book/src/language-features/s-strings.md index dc865316b641..00bb3db5c4a2 100644 --- a/book/src/language-features/s-strings.md +++ b/book/src/language-features/s-strings.md @@ -34,6 +34,15 @@ join s=salaries side:left [ ] ``` +To use brackets in an s-string, use double brackets: + +```prql +from employees +derive [ + has_valid_title = s"regexp_contains(title, '([a-z0-9]*-){{2,}}')" +] +``` + For those who have used python, s-strings are similar to python's f-strings, but the result is SQL, rather than a string literal — a python f-string of `f"average{col}"` where `col="salary"` would produce `"average(salary)"`, with diff --git a/book/tests/prql/language-features/s-strings-3.prql b/book/tests/prql/language-features/s-strings-3.prql new file mode 100644 index 000000000000..782542d16c58 --- /dev/null +++ b/book/tests/prql/language-features/s-strings-3.prql @@ -0,0 +1,4 @@ +from employees +derive [ + has_valid_title = s"regexp_contains(title, '([a-z0-9]*-){{2,}}')" +] diff --git a/book/tests/snapshots/snapshot__run_display_reference_prql@s-strings-3.prql.snap b/book/tests/snapshots/snapshot__run_display_reference_prql@s-strings-3.prql.snap new file mode 100644 index 000000000000..ca0ad0521637 --- /dev/null +++ b/book/tests/snapshots/snapshot__run_display_reference_prql@s-strings-3.prql.snap @@ -0,0 +1,11 @@ +--- +source: book/tests/snapshot.rs +expression: "Item::Query(parse(&prql).unwrap())" +input_file: book/tests/prql/language-features/s-strings-3.prql +--- +prql dialect:generic + +from `employees` +derive [has_valid_title = s"regexp_contains(title, '([a-z0-9]*-){2,}')"] + + diff --git a/book/tests/snapshots/snapshot__run_reference_prql@s-strings-3.prql.snap b/book/tests/snapshots/snapshot__run_reference_prql@s-strings-3.prql.snap new file mode 100644 index 000000000000..99d07fe1e173 --- /dev/null +++ b/book/tests/snapshots/snapshot__run_reference_prql@s-strings-3.prql.snap @@ -0,0 +1,10 @@ +--- +source: book/tests/snapshot.rs +expression: sql +input_file: book/tests/prql/language-features/s-strings-3.prql +--- +SELECT + employees.*, + regexp_contains(title, '([a-z0-9]*-){2,}') AS has_valid_title +FROM + employees diff --git a/prql-compiler/src/parser.rs b/prql-compiler/src/parser.rs index 03f526289909..6cea4a7561a2 100644 --- a/prql-compiler/src/parser.rs +++ b/prql-compiler/src/parser.rs @@ -371,10 +371,12 @@ fn ast_of_interpolate_items(pair: Pair) -> Result> { pair.into_inner() .map(|x| { Ok(match x.as_rule() { - Rule::interpolate_string_inner => InterpolateItem::String(x.as_str().to_string()), - // Rule::interpolate_string_inner | Rule::jinja_string_inner => { - // InterpolateItem::String(x.as_str().to_string()) - // } + Rule::interpolate_string_inner_literal + // The double bracket literals are already stripped of their + // outer brackets by pest, so we pass them through as strings. + | Rule::interpolate_double_bracket_literal => { + InterpolateItem::String(x.as_str().to_string()) + } _ => InterpolateItem::Expr(Box::new(ast_of_parse_pair(x)?.unwrap())), }) }) @@ -609,6 +611,18 @@ Canada Ok(()) } + #[test] + fn test_parse_s_string_brackets() -> Result<()> { + // For crystal variables + assert_yaml_snapshot!(ast_of_string(r#"s"{{?crystal_var}}""#, Rule::expr_call)?, @r###" + --- + SString: + - String: "{?crystal_var}" + "###); + + Ok(()) + } + #[test] fn test_parse_jinja() -> Result<()> { assert_yaml_snapshot!(ast_of_string(r#" diff --git a/prql-compiler/src/prql.pest b/prql-compiler/src/prql.pest index 89ad3ccd5a89..e3c48c3d1839 100644 --- a/prql-compiler/src/prql.pest +++ b/prql-compiler/src/prql.pest @@ -117,7 +117,7 @@ opening_quote = _{ PUSH(multi_quote) | PUSH(single_quote) } // PEEK refers to the opening quote; `"` or `'` or multiple quotes. string_inner = { ( !( PEEK ) ~ ANY )+ } // Either > 3 quotes, or just one. Currently both of those can be multiline. -string = ${ ( opening_quote ) ~ string_inner ~ POP } +string = ${ opening_quote ~ string_inner ~ POP } number = ${ operator_add? ~ ( ASCII_DIGIT )+ ~ ("." ~ ( ASCII_DIGIT )+)? } @@ -139,9 +139,16 @@ operator_compare = ${ "==" | "!=" | ">=" | "<=" | ">" | "<" } operator_logical = ${ ("and" | "or") ~ &WHITESPACE } operator_coalesce = ${ "??" } -s_string = ${ "s" ~ opening_quote ~ ( interpolate_string_inner | ( "{" ~ expr_call ~ "}" ))* ~ POP } -f_string = ${ "f" ~ opening_quote ~ ( interpolate_string_inner | ( "{" ~ expr_call ~ "}" ))* ~ POP } -interpolate_string_inner = { ( !( PEEK | "{" ) ~ ANY )+ } +// If we have lots more string prefixes then we could just have a type +// `prefixed` string and parse in the parser, but manageable for now. +s_string = ${ "s" ~ opening_quote ~ interpolate_string_inner ~ POP } +f_string = ${ "f" ~ opening_quote ~ interpolate_string_inner ~ POP } +interpolate_string_inner = _{ ( interpolate_string_inner_literal | interpolate_double_bracket | ( "{" ~ expr_call ~ "}" ))* } +// We want to strip the outer `{}` of `{{}}`, so we make a silent rule and then +// an inner non-silent rule. +interpolate_double_bracket = _{ "{" ~ interpolate_double_bracket_literal ~ "}" } +interpolate_double_bracket_literal = { "{" ~ ( !"}}" ~ ANY )+ ~ "}" } +interpolate_string_inner_literal = { ( !( PEEK | "{" ) ~ ANY )+ } interval_kind = { "microseconds" | "milliseconds" | "seconds" | "minutes" | "hours" | "days" | "weeks" | "months" | "years" } interval = ${ number ~ interval_kind } diff --git a/prql-compiler/src/utils.rs b/prql-compiler/src/utils.rs index 21232e3a8cdc..36ad930163a9 100644 --- a/prql-compiler/src/utils.rs +++ b/prql-compiler/src/utils.rs @@ -28,7 +28,7 @@ where // consumed; is there a way around this? I guess we could show // the items after the second, which is kinda weird. Some(Position::First(_)) => Err(anyhow!("Expected only one element, but found more.",)), - None => Err(anyhow!("Expected only one element, but found none.",)), + None => Err(anyhow!("Expected one element, but found none.",)), _ => unreachable!(), } }