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, ¶meter.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 ¶meter.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, ¶meter.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 :
|