Line data Source code
1 : //! Rewrite the `@param` tags of a script, the values of its inputs: `@param :name = <literal>`, the literal in the
2 : //! Hyper dialect (`sql_literal`).
3 : //!
4 : //! The value of an input is a tag of any statement: the first `@param` tag of the name in statement order is replaced
5 : //! where it is, the other tags of the name are removed. Without a tag, a new one goes into the annotation of the
6 : //! statement that uses the input first, before its `@vis` tags. Removing the value removes every tag of the name.
7 :
8 : use crate::analyzer::param_tags::{split_param_tag, PARAM_TAG};
9 : use crate::annotation::{rewrite_script_tag, AnnotationTag};
10 : use crate::error::{Error, Result};
11 : use crate::sql_literal::SqlLiteral;
12 : use crate::vis_rewrite::is_vis_family;
13 : use dashql_parser::parser::ParsedScript;
14 :
15 : /// Is a tag the `@param` tag of a name?
16 26 : fn is_input(tag: &AnnotationTag, name: &str) -> bool {
17 26 : tag.name == PARAM_TAG && split_param_tag(&tag.text()).is_ok_and(|parts| parts.name == name)
18 26 : }
19 :
20 : /// The parameter as written in a tag, `:min`, double-quoted if it is no plain word, `:"foo 2"`
21 12 : fn tag_name(name: &str) -> String {
22 38 : if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
23 11 : format!(":{name}")
24 : } else {
25 1 : format!(":\"{}\"", name.replace('"', ""))
26 : }
27 12 : }
28 :
29 : /// The `@param` tag of a value
30 12 : pub fn param_tag(name: &str, literal: &SqlLiteral) -> AnnotationTag {
31 12 : AnnotationTag::new(PARAM_TAG, vec![format!("{} = {}", tag_name(name), literal.to_hyper())])
32 12 : }
33 :
34 : /// Set or remove the value of an input, a literal in the Hyper dialect. `first_use` is the statement that uses the
35 : /// input first, a new tag goes there, the last statement if unknown.
36 17 : pub fn rewrite_param_tag(text: &str, parsed: &ParsedScript, name: &str, literal: Option<&str>, first_use: Option<usize>) -> Result<String> {
37 17 : let literal = literal.map(SqlLiteral::parse).transpose().map_err(Error::invalid_argument)?;
38 15 : let tag = literal.map(|literal| param_tag(name, &literal));
39 : // After the other inputs, before the visualization
40 15 : let insert_at = |tags: &[AnnotationTag]| {
41 8 : tags.iter().rposition(|t| t.name == PARAM_TAG).map(|i| i + 1).or_else(|| tags.iter().position(is_vis_family)).unwrap_or(tags.len())
42 8 : };
43 30 : rewrite_script_tag(text, parsed, |t| is_input(t, name), tag, first_use, insert_at)
44 17 : }
45 :
46 : #[cfg(test)]
47 : mod tests {
48 : use super::rewrite_param_tag;
49 : use dashql_parser::parser::parse;
50 : use dashql_parser::scanner::scan;
51 :
52 10 : fn rewrite(text: &str, name: &str, value: Option<&str>, first_use: Option<usize>) -> String {
53 10 : let parsed = parse(std::sync::Arc::new(scan(text, 0, 0)));
54 10 : rewrite_param_tag(text, &parsed, name, value, first_use).unwrap()
55 10 : }
56 :
57 : #[test]
58 1 : fn adds_a_tag_to_the_first_use() {
59 1 : assert_eq!(rewrite("select 1;\nselect :min", "min", Some("10"), Some(1)), "select 1;\n/** @param :min = 10 */\nselect :min");
60 1 : assert_eq!(rewrite("select :span", "span", Some("INTERVAL '3' DAY"), None), "/** @param :span = interval '3' day */\nselect :span");
61 1 : }
62 :
63 : #[test]
64 1 : fn replaces_the_tag_in_place() {
65 1 : assert_eq!(rewrite("/* @param :min = 10 */\nselect :min", "min", Some("20"), None), "/** @param :min = 20 */\nselect :min");
66 1 : assert_eq!(
67 1 : rewrite("/** @param :a = 1\n * @param :min = 10\n * @vis table */\nselect :min", "min", Some("'x y'"), None),
68 : "/**\n * @param :a = 1\n * @param :min = 'x y'\n * @vis table\n */\nselect :min"
69 : );
70 : // An unset input gets its value
71 1 : assert_eq!(rewrite("/* @param :min */\nselect :min", "min", Some("1"), None), "/** @param :min = 1 */\nselect :min");
72 1 : }
73 :
74 : #[test]
75 1 : fn adds_before_the_visualization() {
76 1 : assert_eq!(
77 1 : rewrite("/* @vis table */\nselect :min", "min", Some("3"), Some(0)),
78 : "/**\n * @param :min = 3\n * @vis table\n */\nselect :min"
79 : );
80 1 : }
81 :
82 : #[test]
83 1 : fn removes_duplicates_and_values() {
84 1 : assert_eq!(
85 1 : rewrite("/* @param :min = 1 */\nselect 1;\n/* @param :min = 2 */\nselect :min", "min", Some("3"), Some(1)),
86 : "/** @param :min = 3 */\nselect 1;\nselect :min"
87 : );
88 1 : assert_eq!(rewrite("/* @param :min = 1 */\nselect :min", "min", None, None), "select :min");
89 1 : assert_eq!(rewrite("select :min", "min", None, None), "select :min");
90 1 : }
91 :
92 : #[test]
93 1 : fn quotes_names_and_rejects_expressions() {
94 1 : assert_eq!(rewrite("select :\"foo 2\"", "foo 2", Some("'a'"), None), "/** @param :\"foo 2\" = 'a' */\nselect :\"foo 2\"");
95 1 : let parsed = parse(std::sync::Arc::new(scan("select :min", 0, 0)));
96 1 : assert!(rewrite_param_tag("select :min", &parsed, "min", Some("1 + 1"), None).is_err());
97 1 : }
98 : }
|