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

            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 std::collections::BTreeMap;
      23              : use std::sync::Arc;
      24              : 
      25              : /// Where the type of an input comes from
      26              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
      27              : pub enum InputTypeSource {
      28              :     /// A cast of the parameter in a script
      29              :     Cast,
      30              :     /// The literal of the value
      31              :     Literal,
      32              :     /// Neither, the input is text until it has a value
      33              :     None,
      34              : }
      35              : 
      36              : /// A statement that uses an input
      37              : #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
      38              : pub struct InputConsumer {
      39              :     /// The script index of the request
      40              :     pub script: u32,
      41              :     /// The statement
      42              :     pub statement: u32,
      43              : }
      44              : 
      45              : /// An input of the session
      46              : #[derive(Clone, Debug, PartialEq)]
      47              : pub struct SessionInput {
      48              :     /// The name of the parameter
      49              :     pub name: Arc<str>,
      50              :     /// The type
      51              :     pub sql_type: SqlType,
      52              :     /// Where the type comes from
      53              :     pub type_source: InputTypeSource,
      54              :     /// The value of the first `@param` tag with a value in session order, None if no script has one
      55              :     pub literal: Option<SqlLiteral>,
      56              :     /// The statements that use the input, in session order
      57              :     pub consumers: Vec<InputConsumer>,
      58              :     /// The problems of the input, e.g. casts to different types
      59              :     pub warnings: Vec<String>,
      60              : }
      61              : 
      62              : /// The inputs of a session, in the order of their first use
      63              : #[derive(Clone, Debug, Default, PartialEq)]
      64              : pub struct SessionInputs {
      65              :     pub inputs: Vec<SessionInput>,
      66              : }
      67              : 
      68              : impl SessionInputs {
      69              :     /// An input by name
      70           26 :     pub fn get(&self, name: &str) -> Option<&SessionInput> {
      71           34 :         self.inputs.iter().find(|i| &*i.name == name)
      72           26 :     }
      73              : 
      74              :     /// The value that a script binds to an input: the value of its own tag, the value of the session otherwise
      75          156 :     pub fn value_of<'a>(&'a self, analyzed: &'a AnalyzedScript, name: &str) -> Option<&'a SqlLiteral> {
      76          279 :         if let Some(tag) = analyzed.param_values.iter().find(|t| &*t.name == name) {
      77          130 :             return tag.literal.as_ref();
      78           26 :         }
      79           26 :         self.get(name).and_then(|i| i.literal.as_ref())
      80          156 :     }
      81              : }
      82              : 
      83              : /// Collect the inputs of the scripts of a session that have a current analysis
      84          478 : pub fn collect_session_inputs(request: &SessionPlanRequest) -> SessionInputs {
      85          478 :     let mut inputs: Vec<SessionInput> = Vec::new();
      86          478 :     let mut index: BTreeMap<Arc<str>, usize> = BTreeMap::new();
      87          951 :     for (si, script) in request.scripts.iter().enumerate() {
      88          951 :         let Some(p) = Prepared::lenient(script.script) else { continue };
      89          936 :         for parameter in &p.analyzed.parameters {
      90          407 :             let (ParameterKind::Scalar, Some(name)) = (parameter.kind, &parameter.name) else { continue };
      91              :             // Positional parameters are no inputs
      92           74 :             if name.parse::<u32>().is_ok() {
      93            1 :                 continue;
      94           73 :             }
      95           73 :             let i = *index.entry(name.clone()).or_insert_with(|| {
      96           62 :                 inputs.push(SessionInput {
      97           62 :                     name: name.clone(),
      98           62 :                     sql_type: SqlType::scalar(SqlTypeKind::Text),
      99           62 :                     type_source: InputTypeSource::None,
     100           62 :                     literal: None,
     101           62 :                     consumers: Vec::new(),
     102           62 :                     warnings: Vec::new(),
     103           62 :                 });
     104           62 :                 inputs.len() - 1
     105           62 :             });
     106           73 :             let input = &mut inputs[i];
     107           73 :             if let Some(statement) = parameter.ast_statement_id {
     108           73 :                 let consumer = InputConsumer { script: si as u32, statement };
     109           73 :                 if !input.consumers.contains(&consumer) {
     110           72 :                     input.consumers.push(consumer);
     111           72 :                 }
     112            0 :             }
     113              :             // The first cast in session order
     114            5 :             match &parameter.cast_type {
     115           14 :                 Some(cast) if input.type_source == InputTypeSource::None => {
     116            9 :                     input.sql_type = cast.clone();
     117            9 :                     input.type_source = InputTypeSource::Cast;
     118            9 :                 }
     119            5 :                 Some(cast) if input.sql_type != *cast => {
     120            2 :                     let warning = format!("input :{name} is cast to {} and to {cast}", input.sql_type);
     121            2 :                     if !input.warnings.contains(&warning) {
     122            1 :                         input.warnings.push(warning);
     123            1 :                     }
     124              :                 }
     125           62 :                 _ => {}
     126              :             }
     127              :         }
     128              :     }
     129              :     // The values, the first tag with a value in session order
     130          951 :     for script in request.scripts {
     131          951 :         let Some(p) = Prepared::lenient(script.script) else { continue };
     132          936 :         for tag in &p.analyzed.param_values {
     133           60 :             if let (Some(&i), Some(literal)) = (index.get(&tag.name), &tag.literal) {
     134           55 :                 inputs[i].literal.get_or_insert_with(|| literal.clone());
     135            5 :             }
     136              :         }
     137              :     }
     138              :     // An input without a cast has the type of its literal
     139          478 :     for input in inputs.iter_mut().filter(|i| i.type_source == InputTypeSource::None) {
     140           53 :         if let Some(mut literal_type) = input.literal.as_ref().and_then(SqlLiteral::sql_type) {
     141           41 :             if let SqlTypeKind::Interval { fields, .. } = &mut literal_type.kind {
     142            8 :                 *fields = None;
     143           33 :             }
     144           41 :             input.sql_type = literal_type;
     145           41 :             input.type_source = InputTypeSource::Literal;
     146           12 :         }
     147              :     }
     148          478 :     SessionInputs { inputs }
     149          478 : }
     150              : 
     151              : /// The bindings of the inputs of a statement: the text span of every named scalar parameter and its literal in the
     152              : /// dialect of the connection, and the problems with their text spans
     153              : pub(crate) struct InputBindings {
     154              :     pub edits: Vec<(TextSpan, String)>,
     155              :     pub errors: Vec<(TextSpan, String)>,
     156              :     /// The bound values by name in the Hyper dialect, for the result key
     157              :     pub values: BTreeMap<Arc<str>, String>,
     158              : }
     159              : 
     160              : /// Bind the inputs of a statement, every statement if None
     161         2105 : pub(crate) fn bind_inputs(inputs: &SessionInputs, p: &Prepared, statement: Option<u32>, dialect: FormattingDialect) -> InputBindings {
     162         2105 :     let mut out = InputBindings { edits: Vec::new(), errors: Vec::new(), values: BTreeMap::new() };
     163         2105 :     for parameter in &p.analyzed.parameters {
     164          742 :         let (ParameterKind::Scalar, Some(name)) = (parameter.kind, &parameter.name) else { continue };
     165          160 :         if name.parse::<u32>().is_ok() || (statement.is_some() && parameter.ast_statement_id != statement) {
     166            4 :             continue;
     167          156 :         }
     168          156 :         let span = resolve_text_span(&p.scanned.text, &p.scanned.symbols, parameter.location);
     169          156 :         let Some(literal) = inputs.value_of(p.analyzed, name) else {
     170           24 :             out.errors.push((span, format!("input :{name} has no value")));
     171           24 :             continue;
     172              :         };
     173          132 :         match literal.print(dialect) {
     174          132 :             Ok(text) => {
     175          132 :                 out.values.insert(name.clone(), literal.to_hyper());
     176          132 :                 out.edits.push((span, text));
     177          132 :             }
     178            0 :             Err(e) => out.errors.push((span, format!("input :{name}: {e}"))),
     179              :         }
     180              :     }
     181         2105 :     out
     182         2105 : }
     183              : 
        

Generated by: LCOV version 2.0-1