LCOV - code coverage report
Current view: top level - dashql-core/src - param_rewrite.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 100.0 % 48 48
Test Date: 2026-11-06 17:46:50

            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              : }
        

Generated by: LCOV version 2.0-1