LCOV - code coverage report
Current view: top level - dashql-core-session/src - session_inputs.rs Coverage Total Hit
Test: DashQL d805a4b Lines: 97.8 % 92 90
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : //! The inputs of a session: the named scalar parameters of its scripts and their values.
       2              : //!
       3              : //! Every named scalar parameter `:min` of a script with a current analysis is an input of the session, one per name.
       4              : //! Its value is the `@param :min = <literal>` tag of a script (`analyzer::param_tags`), a literal in the Hyper dialect
       5              : //! (`sql_literal`). The scripts of the session that use the input hold the same value. A script binds the value of its
       6              : //! own tag, a script without one binds the value of the first tag in session order, so a script holds its values on
       7              : //! its own and the session reads one value per input.
       8              : //!
       9              : //! The type of an input is the type of a cast of the parameter, `:min::int`, the first cast in session order, or the
      10              : //! type of its literal: `interval '3' day` is an interval, `10` a bigint and `'EU'` text. The unit of an interval is
      11              : //! part of its value, a control picks it, so the type of the literal of an interval is `interval` without fields.
      12              : //!
      13              : //! The planner binds the literal in the dialect of the connection of the statement, `INTERVAL '3' DAY` on Trino. A
      14              : //! literal that the dialect can't express and an input without a value are errors of the plan.
      15              : 
      16              : use crate::planner::{Prepared, SessionPlanRequest};
      17              : use dashql_buffers::formatting::FormattingDialect;
      18              : use dashql_core::analyzer::{AnalyzedScript, ParameterKind, SqlType, SqlTypeKind};
      19              : use dashql_core::sql_literal::SqlLiteral;
      20              : use dashql_parser::parser::context::resolve_text_span;
      21              : use dashql_parser::span::TextSpan;
      22              : use serde_json::Value;
      23              : use std::collections::BTreeMap;
      24              : use std::sync::Arc;
      25              : 
      26              : /// Where the type of an input comes from
      27              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
      28              : pub enum InputTypeSource {
      29              :     /// A cast of the parameter in a script
      30              :     Cast,
      31              :     /// The literal of the value
      32              :     Literal,
      33              :     /// Neither, the input is text until it has a value
      34              :     None,
      35              : }
      36              : 
      37              : /// A statement that uses an input
      38              : #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
      39              : pub struct InputConsumer {
      40              :     /// The script index of the request
      41              :     pub script: u32,
      42              :     /// The statement
      43              :     pub statement: u32,
      44              : }
      45              : 
      46              : /// An input of the session
      47              : #[derive(Clone, Debug, PartialEq)]
      48              : pub struct SessionInput {
      49              :     /// The name of the parameter
      50              :     pub name: Arc<str>,
      51              :     /// The type
      52              :     pub sql_type: SqlType,
      53              :     /// Where the type comes from
      54              :     pub type_source: InputTypeSource,
      55              :     /// The value of the first `@param` tag with a value in session order, None if no script has one
      56              :     pub literal: Option<SqlLiteral>,
      57              :     /// The statements that use the input, in session order
      58              :     pub consumers: Vec<InputConsumer>,
      59              :     /// The problems of the input, e.g. casts to different types
      60              :     pub warnings: Vec<String>,
      61              : }
      62              : 
      63              : /// The inputs of a session, in the order of their first use
      64              : #[derive(Clone, Debug, Default, PartialEq)]
      65              : pub struct SessionInputs {
      66              :     pub inputs: Vec<SessionInput>,
      67              : }
      68              : 
      69              : impl SessionInputs {
      70              :     /// An input by name
      71           35 :     pub fn get(&self, name: &str) -> Option<&SessionInput> {
      72           43 :         self.inputs.iter().find(|i| &*i.name == name)
      73           35 :     }
      74              : 
      75              :     /// The value that a script binds to an input: the value of its own tag, the value of the session otherwise
      76          192 :     pub fn value_of<'a>(&'a self, analyzed: &'a AnalyzedScript, name: &str) -> Option<&'a SqlLiteral> {
      77          333 :         if let Some(tag) = analyzed.param_values.iter().find(|t| &*t.name == name) {
      78          157 :             return tag.literal.as_ref();
      79           35 :         }
      80           35 :         self.get(name).and_then(|i| i.literal.as_ref())
      81          192 :     }
      82              : }
      83              : 
      84              : /// Collect the inputs of the scripts of a session that have a current analysis
      85          516 : pub fn collect_session_inputs(request: &SessionPlanRequest) -> SessionInputs {
      86          516 :     let mut inputs: Vec<SessionInput> = Vec::new();
      87          516 :     let mut index: BTreeMap<Arc<str>, usize> = BTreeMap::new();
      88         1018 :     for (si, script) in request.scripts.iter().enumerate() {
      89         1018 :         let Some(p) = Prepared::lenient(script.script) else { continue };
      90         1003 :         for parameter in &p.analyzed.parameters {
      91          430 :             let (ParameterKind::Scalar, Some(name)) = (parameter.kind, &parameter.name) else { continue };
      92              :             // Positional parameters are no inputs
      93           86 :             if name.parse::<u32>().is_ok() {
      94            1 :                 continue;
      95           85 :             }
      96           85 :             let i = *index.entry(name.clone()).or_insert_with(|| {
      97           74 :                 inputs.push(SessionInput {
      98           74 :                     name: name.clone(),
      99           74 :                     sql_type: SqlType::scalar(SqlTypeKind::Text),
     100           74 :                     type_source: InputTypeSource::None,
     101           74 :                     literal: None,
     102           74 :                     consumers: Vec::new(),
     103           74 :                     warnings: Vec::new(),
     104           74 :                 });
     105           74 :                 inputs.len() - 1
     106           74 :             });
     107           85 :             let input = &mut inputs[i];
     108           85 :             if let Some(statement) = parameter.ast_statement_id {
     109           85 :                 let consumer = InputConsumer { script: si as u32, statement };
     110           85 :                 if !input.consumers.contains(&consumer) {
     111           84 :                     input.consumers.push(consumer);
     112           84 :                 }
     113            0 :             }
     114              :             // The first cast in session order
     115            5 :             match &parameter.cast_type {
     116           14 :                 Some(cast) if input.type_source == InputTypeSource::None => {
     117            9 :                     input.sql_type = cast.clone();
     118            9 :                     input.type_source = InputTypeSource::Cast;
     119            9 :                 }
     120            5 :                 Some(cast) if input.sql_type != *cast => {
     121            2 :                     let warning = format!("input :{name} is cast to {} and to {cast}", input.sql_type);
     122            2 :                     if !input.warnings.contains(&warning) {
     123            1 :                         input.warnings.push(warning);
     124            1 :                     }
     125              :                 }
     126           74 :                 _ => {}
     127              :             }
     128              :         }
     129              :     }
     130              :     // The values, the first tag with a value in session order
     131         1018 :     for script in request.scripts {
     132         1018 :         let Some(p) = Prepared::lenient(script.script) else { continue };
     133         1003 :         for tag in &p.analyzed.param_values {
     134           69 :             if let (Some(&i), Some(literal)) = (index.get(&tag.name), &tag.literal) {
     135           64 :                 inputs[i].literal.get_or_insert_with(|| literal.clone());
     136            5 :             }
     137              :         }
     138              :     }
     139              :     // An input without a cast has the type of its literal
     140          516 :     for input in inputs.iter_mut().filter(|i| i.type_source == InputTypeSource::None) {
     141           65 :         if let Some(mut literal_type) = input.literal.as_ref().and_then(SqlLiteral::sql_type) {
     142           50 :             if let SqlTypeKind::Interval { fields, .. } = &mut literal_type.kind {
     143            8 :                 *fields = None;
     144           42 :             }
     145           50 :             input.sql_type = literal_type;
     146           50 :             input.type_source = InputTypeSource::Literal;
     147           15 :         }
     148              :     }
     149          516 :     SessionInputs { inputs }
     150          516 : }
     151              : 
     152              : /// The bindings of the inputs of a statement: the text span of every named scalar parameter and its literal in the
     153              : /// dialect of the connection, and the problems with their text spans
     154              : pub(crate) struct InputBindings {
     155              :     pub edits: Vec<(TextSpan, String)>,
     156              :     pub errors: Vec<(TextSpan, String)>,
     157              :     /// The bound values by name in the Hyper dialect, for the result key
     158              :     pub values: BTreeMap<Arc<str>, String>,
     159              : }
     160              : 
     161              : /// Describe the client function call of a statement for the host, with the values that the script binds in place of its
     162              : /// parameters. A parameter without a value stays null, `bind_inputs` makes it an error of the plan.
     163           51 : pub(crate) fn describe_call(inputs: &SessionInputs, analyzed: &AnalyzedScript, statement: u32) -> Option<String> {
     164           51 :     let mut description = analyzed.describe_client_function_call(statement)?;
     165           48 :     let bindings = description.as_object_mut()?.remove("bindings");
     166           48 :     for binding in bindings.as_ref().and_then(Value::as_array).into_iter().flatten() {
     167           12 :         let (Some(pointer), Some(param)) = (binding["pointer"].as_str(), binding["param"].as_str()) else { continue };
     168           12 :         let (Some(literal), Some(option)) = (inputs.value_of(analyzed, param), description["options"].pointer_mut(pointer)) else { continue };
     169            9 :         *option = literal.to_json();
     170              :     }
     171           48 :     Some(description.to_string())
     172           51 : }
     173              : 
     174              : /// Bind the inputs of a statement, every statement if None
     175         2253 : pub(crate) fn bind_inputs(inputs: &SessionInputs, p: &Prepared, statement: Option<u32>, dialect: FormattingDialect) -> InputBindings {
     176         2253 :     let mut out = InputBindings { edits: Vec::new(), errors: Vec::new(), values: BTreeMap::new() };
     177         2253 :     for parameter in &p.analyzed.parameters {
     178          788 :         let (ParameterKind::Scalar, Some(name)) = (parameter.kind, &parameter.name) else { continue };
     179          193 :         if name.parse::<u32>().is_ok() || (statement.is_some() && parameter.ast_statement_id != statement) {
     180           13 :             continue;
     181          180 :         }
     182          180 :         let span = resolve_text_span(&p.scanned.text, &p.scanned.symbols, parameter.location);
     183          180 :         let Some(literal) = inputs.value_of(p.analyzed, name) else {
     184           30 :             out.errors.push((span, format!("input :{name} has no value")));
     185           30 :             continue;
     186              :         };
     187          150 :         match literal.print(dialect) {
     188          150 :             Ok(text) => {
     189          150 :                 out.values.insert(name.clone(), literal.to_hyper());
     190          150 :                 out.edits.push((span, text));
     191          150 :             }
     192            0 :             Err(e) => out.errors.push((span, format!("input :{name}: {e}"))),
     193              :         }
     194              :     }
     195         2253 :     out
     196         2253 : }
     197              : 
        

Generated by: LCOV version 2.0-1