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

            Line data    Source code
       1              : //! Script references.
       2              : //!
       3              : //! A table parameter `:foo` references the script `foo.sql` and reads the result of its terminal query. The
       4              : //! connections of the two scripts decide what a reference becomes:
       5              : //!
       6              : //! A referenced script is a single SELECT, a reference never runs commands.
       7              : //!
       8              : //! - Within one connection the query is inlined into the statement as a common table expression,
       9              : //!   `with foo as (<query of foo>) select * from foo`.
      10              : //! - A statement of the LOCAL connection (the embedded Hyper) scans the result of a script of another
      11              : //!   connection, which the host writes to a Parquet file and mounts:
      12              : //!   `with foo as (select * from external('<path>', format => 'parquet')) select * from foo`.
      13              : //! - A statement of a REMOTE connection cannot read a script of another connection.
      14              : //!
      15              : //! A table parameter `:sales` can also read the attachment `sales.parquet` of the session. Only a statement of the
      16              : //! LOCAL connection reads attachments, it scans the file below the attachment prefix that the host mounts:
      17              : //! `with sales as (select * from external('<prefix>/sales.parquet', format => 'parquet')) select * from sales`.
      18              : //!
      19              : //! The common table expressions of a statement are flattened: the references of an inlined query are resolved
      20              : //! in the context of the statement, every script appears once, after the scripts it references. They go into
      21              : //! the outermost SELECT around the references, before the common table expressions the statement already has.
      22              : 
      23              : use crate::compiler::read_executable_text_with;
      24              : use crate::planner::{runs_on_request, script_label, ConnectionKind, Prepared, SessionPlanRequest};
      25              : use dashql_buffers::execution::ExecutionGraphMaterializationT;
      26              : use dashql_buffers::parser::{AttributeKey as Key, NodeType, StatementType};
      27              : use dashql_core::analyzer::ParameterKind;
      28              : use dashql_core::table_parameters::{colliding_names, find_table_parameter, TableParameterLookup, TableParameterSource};
      29              : use dashql_core::utils::quote_identifier;
      30              : use dashql_parser::attrs;
      31              : use dashql_parser::keywords::Keyword;
      32              : use dashql_parser::parser::context::resolve_text_span;
      33              : use dashql_parser::span::TextSpan;
      34              : use std::collections::{BTreeMap, BTreeSet};
      35              : 
      36              : /// Receives the errors of script references
      37              : pub(crate) trait ReferenceErrors {
      38              :     /// Report an error at the text of a script
      39              :     fn error(&mut self, script: u32, text_span: Option<TextSpan>, message: String);
      40              : }
      41              : 
      42              : /// What a script reference becomes
      43              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
      44              : pub enum ReferenceKind {
      45              :     /// The terminal query of the referenced script is inlined into the statement
      46              :     Inline,
      47              :     /// The statement scans the materialized result of the referenced script
      48              :     Scan,
      49              :     /// The reference has errors, it cannot be planned
      50              :     Invalid,
      51              :     /// The statement scans an attachment of the session
      52              :     Attachment,
      53              : }
      54              : 
      55              : /// A table parameter that references a script or an attachment
      56              : #[derive(Clone, Debug)]
      57              : pub(crate) struct Reference {
      58              :     /// The statement of the parameter
      59              :     pub statement_id: u32,
      60              :     /// The parameter node
      61              :     pub ast_node_id: u32,
      62              :     /// The parameter in the text, with the prefix
      63              :     pub text_span: TextSpan,
      64              :     /// The parameter name
      65              :     pub name: String,
      66              :     /// The referenced script
      67              :     pub producer: Option<u32>,
      68              :     /// The referenced attachment
      69              :     pub attachment: Option<u32>,
      70              :     /// What the reference becomes
      71              :     pub kind: ReferenceKind,
      72              : }
      73              : 
      74              : impl Reference {
      75              :     /// What the reference reads
      76          929 :     pub(crate) fn source(&self) -> Option<TableParameterSource> {
      77          929 :         match (self.producer, self.attachment) {
      78          837 :             (Some(producer), _) => Some(TableParameterSource::Script(producer)),
      79           76 :             (None, Some(attachment)) => Some(TableParameterSource::Attachment(attachment)),
      80           16 :             (None, None) => None,
      81              :         }
      82          929 :     }
      83              : }
      84              : 
      85              : /// The label of an attachment in messages, its file name
      86           11 : pub(crate) fn attachment_label(request: &SessionPlanRequest, attachment: u32) -> String {
      87           11 :     match request.attachments.get(attachment as usize) {
      88           11 :         Some(a) => a.file.to_string(),
      89            0 :         None => format!("attachment {attachment}"),
      90              :     }
      91           11 : }
      92              : 
      93              : /// The label of what a table parameter reads in messages
      94           20 : pub(crate) fn source_label(request: &SessionPlanRequest, source: TableParameterSource) -> String {
      95           20 :     match source {
      96           15 :         TableParameterSource::Script(script) => script_label(request, script),
      97            5 :         TableParameterSource::Attachment(attachment) => attachment_label(request, attachment),
      98              :     }
      99           20 : }
     100              : 
     101              : /// The source of the name `index` among the names of the scripts followed by the names of the attachments
     102          516 : fn source_of(request: &SessionPlanRequest, index: usize) -> TableParameterSource {
     103          516 :     match index.checked_sub(request.scripts.len()) {
     104          473 :         None => TableParameterSource::Script(index as u32),
     105           43 :         Some(attachment) => TableParameterSource::Attachment(attachment as u32),
     106              :     }
     107          516 : }
     108              : 
     109              : /// The names of the table parameters of a request: the scripts in session order, then the attachments
     110          785 : fn table_parameter_names<'a>(request: &'a SessionPlanRequest) -> impl Iterator<Item = &'a str> + Clone {
     111          785 :     request.scripts.iter().map(|s| s.name).chain(request.attachments.iter().map(|a| a.name))
     112          785 : }
     113              : 
     114              : /// The script references of the scripts of a plan
     115              : pub(crate) struct ScriptReferences {
     116              :     /// The references of every script in text order
     117              :     by_script: Vec<Vec<Reference>>,
     118              : }
     119              : 
     120              : impl ScriptReferences {
     121          755 :     pub(crate) fn new(script_count: usize) -> Self {
     122          755 :         Self { by_script: vec![Vec::new(); script_count] }
     123          755 :     }
     124              : 
     125              :     /// The references of a script
     126         9230 :     pub(crate) fn of(&self, script: u32) -> &[Reference] {
     127         9230 :         &self.by_script[script as usize]
     128         9230 :     }
     129              : 
     130              :     /// The scripts that a script references, in the order of their first reference
     131          914 :     pub(crate) fn producers_of(&self, script: u32) -> Vec<u32> {
     132          914 :         let mut out: Vec<u32> = Vec::new();
     133          914 :         for producer in self.of(script).iter().filter_map(|r| r.producer) {
     134          277 :             if !out.contains(&producer) {
     135          267 :                 out.push(producer);
     136          267 :             }
     137              :         }
     138          914 :         out
     139          914 :     }
     140              : 
     141              :     /// Invalidate the references of scripts that were not prepared. A tolerant plan keeps going after their errors and
     142              :     /// splices no invalid reference.
     143           48 :     pub(crate) fn invalidate_unprepared(&mut self, prepared: &[Option<Prepared>]) {
     144           50 :         for r in self.by_script.iter_mut().flatten() {
     145           32 :             if r.producer.is_some_and(|p| prepared[p as usize].is_none()) {
     146            3 :                 r.kind = ReferenceKind::Invalid;
     147           29 :             }
     148              :         }
     149           48 :     }
     150              : 
     151              :     /// Resolve the table parameters of a script to the scripts and attachments they reference
     152         1448 :     pub(crate) fn resolve(&mut self, reporter: &mut dyn ReferenceErrors, request: &SessionPlanRequest, script: u32, prepared: &Prepared) {
     153         1448 :         let (scanned, parsed, analyzed) = (prepared.scanned, prepared.parsed, prepared.analyzed);
     154         1448 :         let consumer = &request.scripts[script as usize];
     155         1448 :         let mut out = Vec::new();
     156         1448 :         for parameter in analyzed.parameters.iter().filter(|p| p.kind == ParameterKind::Table) {
     157          533 :             let text_span = resolve_text_span(&scanned.text, &scanned.symbols, parameter.location);
     158          533 :             let named = scanned.text.as_bytes().get(text_span.offset as usize) == Some(&b':');
     159          533 :             let Some(statement_id) = parameter.ast_statement_id else { continue };
     160          533 :             let label = &scanned.text[text_span.offset as usize..text_span.end() as usize];
     161          533 :             let name = match (&parameter.name, named) {
     162          523 :                 (Some(name), true) => name.to_string(),
     163              :                 _ => {
     164           10 :                     reporter.error(script, Some(text_span), format!("the positional table parameter {label} cannot reference a script or an attachment"));
     165           10 :                     continue;
     166              :                 }
     167              :             };
     168          523 :             let mut reference = Reference {
     169          523 :                 statement_id,
     170          523 :                 ast_node_id: parameter.ast_node_id,
     171          523 :                 text_span,
     172          523 :                 name: name.clone(),
     173          523 :                 producer: None,
     174          523 :                 attachment: None,
     175          523 :                 kind: ReferenceKind::Invalid,
     176          523 :             };
     177              :             // The table reference of the parameter, a table parameter cannot be the target of a write
     178          523 :             let tableref = &parsed.nodes[parsed.nodes[parameter.ast_node_id as usize].parent() as usize];
     179          523 :             if matches!(tableref.attribute_key(), Key::SQL_INSERT_TARGET | Key::SQL_DELETE_TARGET) {
     180           15 :                 reporter.error(script, Some(text_span), format!("the table parameter {label} cannot be written"));
     181           15 :                 out.push(reference);
     182           15 :                 continue;
     183          508 :             }
     184              :             // An exact name, or a single name that differs in case, among the scripts and the attachments
     185          508 :             let producer = match find_table_parameter(table_parameter_names(request), &name) {
     186          488 :                 TableParameterLookup::Found(i) => match source_of(request, i) {
     187          452 :                     TableParameterSource::Script(producer) => producer as usize,
     188           36 :                     TableParameterSource::Attachment(attachment) => {
     189           36 :                         reference.attachment = Some(attachment);
     190           36 :                         reference.kind = if request.connections[consumer.connection as usize].kind == ConnectionKind::Local {
     191           30 :                             ReferenceKind::Attachment
     192              :                         } else {
     193            6 :                             reporter.error(
     194            6 :                                 script,
     195            6 :                                 Some(text_span),
     196            6 :                                 format!(
     197              :                                     "{} runs on the connection {} and cannot read the attachment {}, only the embedded Hyper reads attachments",
     198            6 :                                     script_label(request, script),
     199            6 :                                     request.connections[consumer.connection as usize].id,
     200            6 :                                     attachment_label(request, attachment)
     201              :                                 ),
     202              :                             );
     203            6 :                             ReferenceKind::Invalid
     204              :                         };
     205           36 :                         out.push(reference);
     206           36 :                         continue;
     207              :                     }
     208              :                 },
     209              :                 TableParameterLookup::Unknown => {
     210           10 :                     reporter.error(script, Some(text_span), format!("unknown script or attachment {label}"));
     211           10 :                     out.push(reference);
     212           10 :                     continue;
     213              :                 }
     214           10 :                 TableParameterLookup::Ambiguous(matches) => {
     215           20 :                     let labels: Vec<String> = matches.iter().map(|&i| source_label(request, source_of(request, i))).collect();
     216           10 :                     reporter.error(script, Some(text_span), format!("the table parameter {label} matches {}", labels.join(" and ")));
     217           10 :                     out.push(reference);
     218           10 :                     continue;
     219              :                 }
     220              :             };
     221          452 :             reference.producer = Some(producer as u32);
     222          452 :             let producer_connection = request.scripts[producer].connection;
     223          452 :             let connection = |c: u32| request.connections[c as usize].id;
     224          452 :             reference.kind = if producer_connection == consumer.connection {
     225          356 :                 ReferenceKind::Inline
     226           96 :             } else if runs_on_request(request.scripts[producer].script) {
     227              :                 // Reading the result on another connection would run the script in full
     228            5 :                 reporter.error(
     229            5 :                     script,
     230            5 :                     Some(text_span),
     231            5 :                     format!(
     232              :                         "{} runs only on request (@manual), the scripts of the connection {} inline it but {} of the connection {} cannot read it",
     233            5 :                         script_label(request, producer as u32),
     234            5 :                         connection(producer_connection),
     235            5 :                         script_label(request, script),
     236            5 :                         connection(consumer.connection)
     237              :                     ),
     238              :                 );
     239            5 :                 ReferenceKind::Invalid
     240           91 :             } else if request.connections[consumer.connection as usize].kind == ConnectionKind::Local {
     241           80 :                 ReferenceKind::Scan
     242              :             } else {
     243           11 :                 reporter.error(
     244           11 :                     script,
     245           11 :                     Some(text_span),
     246           11 :                     format!(
     247              :                         "{} runs on the connection {} and cannot read {} of the connection {}",
     248           11 :                         script_label(request, script),
     249           11 :                         connection(consumer.connection),
     250           11 :                         script_label(request, producer as u32),
     251           11 :                         connection(producer_connection)
     252              :                     ),
     253              :                 );
     254           11 :                 ReferenceKind::Invalid
     255              :             };
     256          452 :             out.push(reference);
     257              :         }
     258         1448 :         self.by_script[script as usize] = out;
     259         1448 :     }
     260              : 
     261              :     /// The scripts that references read, every script after the scripts it references, with what reads them: the
     262              :     /// producer of a reference and, through the terminal query of an inlined script, the producers of its references
     263          628 :     fn closure<'r>(&self, prepared: &[Option<Prepared>], roots: impl IntoIterator<Item = &'r Reference>) -> Vec<(u32, ReferenceKind)> {
     264          773 :         fn visit(this: &ScriptReferences, prepared: &[Option<Prepared>], r: &Reference, visited: &mut BTreeSet<u32>, out: &mut Vec<(u32, ReferenceKind)>) {
     265          773 :             let Some(producer) = r.producer else { return };
     266          742 :             if r.kind == ReferenceKind::Invalid || !visited.insert(producer) {
     267           53 :                 return;
     268          689 :             }
     269          689 :             if r.kind == ReferenceKind::Inline {
     270          555 :                 let terminal = prepared[producer as usize].as_ref().and_then(Prepared::terminal);
     271          555 :                 for nested in this.of(producer).iter().filter(|n| Some(n.statement_id) == terminal) {
     272          131 :                     visit(this, prepared, nested, visited, out);
     273          131 :                 }
     274          134 :             }
     275          689 :             out.push((producer, r.kind));
     276          773 :         }
     277          628 :         let (mut visited, mut out) = (BTreeSet::new(), Vec::new());
     278          642 :         for r in roots {
     279          642 :             visit(self, prepared, r, &mut visited, &mut out);
     280          642 :         }
     281          628 :         out
     282          628 :     }
     283              : 
     284              :     /// Check the referenced scripts: they are a single query, references form no cycle, and a view does not scan a
     285              :     /// materialized result, neither directly nor through the queries it inlines. A reference with errors becomes
     286              :     /// invalid.
     287          698 :     pub(crate) fn check(&mut self, reporter: &mut dyn ReferenceErrors, request: &SessionPlanRequest, prepared: &[Option<Prepared>]) {
     288              :         // The references with errors: (script, index)
     289          698 :         let mut failed: BTreeSet<(u32, usize)> = BTreeSet::new();
     290          698 :         let mut reporter = FailedReferences { reporter, failed: &mut failed, current: (0, 0) };
     291          698 :         let reporter = &mut reporter;
     292         1397 :         for (script, references) in self.by_script.iter().enumerate() {
     293         1397 :             let Some(consumer) = prepared[script].as_ref() else { continue };
     294         1369 :             for (index, r) in references.iter().enumerate() {
     295          485 :                 reporter.current = (script as u32, index);
     296          485 :                 let producer = match r.source() {
     297          436 :                     Some(TableParameterSource::Script(producer)) => {
     298          436 :                         let Some(p) = prepared[producer as usize].as_ref() else { continue };
     299          429 :                         let query = p.parses() && matches!(&p.parsed.statements[..], [statement] if statement.statement_type == StatementType::SELECT);
     300          429 :                         if !query {
     301           33 :                             reporter.error(
     302           33 :                                 script as u32,
     303           33 :                                 Some(r.text_span),
     304           33 :                                 format!("{} is no single SELECT that {} could read", script_label(request, producer), script_label(request, script as u32)),
     305           33 :                             );
     306          396 :                         }
     307          429 :                         Some(producer)
     308              :                     }
     309              :                     // An attachment is a file, it is read like a table
     310           33 :                     Some(TableParameterSource::Attachment(_)) => None,
     311           16 :                     None => continue,
     312              :                 };
     313          462 :                 let anchor = anchor_of(consumer.parsed, r.ast_node_id);
     314          462 :                 if anchor.is_none() {
     315            5 :                     reporter.error(script as u32, Some(r.text_span), "a table parameter must be part of a query".into());
     316          457 :                 }
     317          462 :                 let Some(producer) = producer else { continue };
     318              :                 // The scripts that the reference reads, the nearest first
     319          429 :                 let closure: Vec<(u32, ReferenceKind)> = self.closure(prepared, [r]).into_iter().rev().collect();
     320              :                 // A relation of an inlined query must not resolve to a common table expression of the statement
     321          429 :                 if let (Some(anchor), ReferenceKind::Inline) = (anchor, r.kind) {
     322          337 :                     let visible = visible_ctes(consumer, r.statement_id, anchor);
     323          407 :                     let conflict = closure.iter().filter(|(_, kind)| *kind == ReferenceKind::Inline).find_map(|&(inlined, _)| {
     324          397 :                         let p = prepared[inlined as usize].as_ref()?;
     325          397 :                         let terminal = p.terminal()?;
     326              :                         // The query reads its own common table expressions
     327          390 :                         let (mut own, mut relations) = (BTreeSet::new(), BTreeSet::new());
     328          390 :                         cte_names(p, terminal, &mut own);
     329          390 :                         relation_names(p, terminal, &mut relations);
     330          390 :                         relations.difference(&own).find(|name| visible.contains(*name)).map(|name| (inlined, name.clone()))
     331          397 :                     });
     332          337 :                     if let Some((inlined, relation)) = conflict {
     333           10 :                         reporter.error(
     334           10 :                             script as u32,
     335           10 :                             Some(r.text_span),
     336           10 :                             format!(
     337           10 :                                 "the query of {} reads {relation}, which {} defines as a common table expression there",
     338           10 :                                 script_label(request, inlined),
     339           10 :                                 script_label(request, script as u32)
     340           10 :                             ),
     341           10 :                         );
     342          327 :                     }
     343           92 :                 }
     344          429 :                 let statement_type = consumer.parsed.statements[r.statement_id as usize].statement_type;
     345          429 :                 if statement_type == StatementType::CREATE_VIEW {
     346              :                     // The view scans what the queries it inlines scan
     347           10 :                     let message = |materialized: u32| format!("a view cannot read {} of another connection", script_label(request, materialized));
     348           10 :                     let lifetime = "its result exists only while the statement runs";
     349           15 :                     let scanned = closure.iter().find(|(_, kind)| *kind == ReferenceKind::Scan).map(|&(materialized, _)| materialized);
     350           10 :                     if r.kind == ReferenceKind::Scan {
     351            5 :                         reporter.error(script as u32, Some(r.text_span), format!("{}, {lifetime}", message(producer)));
     352            5 :                     } else if let Some(materialized) = scanned {
     353            5 :                         reporter.error(
     354            5 :                             script as u32,
     355            5 :                             Some(r.text_span),
     356            5 :                             format!("{} through {}, {lifetime}", message(materialized), script_label(request, producer)),
     357            5 :                         );
     358            5 :                     }
     359          419 :                 }
     360              :             }
     361              :         }
     362              :         // Cycles: a reference back to a script on the path
     363              :         #[derive(Clone, Copy, PartialEq)]
     364              :         enum Color {
     365              :             White,
     366              :             Gray,
     367              :             Black,
     368              :         }
     369          698 :         let mut colors = vec![Color::White; self.by_script.len()];
     370          698 :         let mut path: Vec<u32> = Vec::new();
     371          732 :         fn visit(this: &ScriptReferences, reporter: &mut FailedReferences, request: &SessionPlanRequest, script: u32, colors: &mut [Color], path: &mut Vec<u32>) {
     372          732 :             colors[script as usize] = Color::Gray;
     373          732 :             path.push(script);
     374          732 :             for (index, r) in this.of(script).iter().enumerate() {
     375          485 :                 let Some(producer) = r.producer else { continue };
     376          436 :                 match colors[producer as usize] {
     377              :                     Color::Gray => {
     378           17 :                         let begin = path.iter().position(|&s| s == producer).unwrap();
     379           51 :                         let cycle: Vec<String> = path[begin..].iter().chain(std::iter::once(&producer)).map(|&s| script_label(request, s)).collect();
     380           17 :                         reporter.current = (script, index);
     381           17 :                         reporter.error(script, Some(r.text_span), format!("cyclic script reference: {}", cycle.join(" -> ")));
     382              :                     }
     383          314 :                     Color::White => visit(this, reporter, request, producer, colors, path),
     384          105 :                     Color::Black => {}
     385              :                 }
     386              :             }
     387          732 :             path.pop();
     388          732 :             colors[script as usize] = Color::Black;
     389          732 :         }
     390         1397 :         for script in 0..self.by_script.len() as u32 {
     391         1397 :             if colors[script as usize] == Color::White && !self.of(script).is_empty() {
     392          418 :                 visit(self, reporter, request, script, &mut colors, &mut path);
     393          979 :             }
     394              :         }
     395          698 :         for (script, index) in failed {
     396           75 :             self.by_script[script as usize][index].kind = ReferenceKind::Invalid;
     397           75 :         }
     398          698 :     }
     399              : }
     400              : 
     401              : /// Passes the errors of the checked reference on and remembers that it failed
     402              : struct FailedReferences<'r> {
     403              :     reporter: &'r mut dyn ReferenceErrors,
     404              :     failed: &'r mut BTreeSet<(u32, usize)>,
     405              :     /// The checked reference: (script, index)
     406              :     current: (u32, usize),
     407              : }
     408              : 
     409              : impl ReferenceErrors for FailedReferences<'_> {
     410           75 :     fn error(&mut self, script: u32, text_span: Option<TextSpan>, message: String) {
     411           75 :         self.failed.insert(self.current);
     412           75 :         self.reporter.error(script, text_span, message);
     413           75 :     }
     414              : }
     415              : 
     416              : /// A checked table parameter that references a script or an attachment
     417              : #[derive(Clone, Debug, PartialEq, Eq)]
     418              : pub struct CheckedReference {
     419              :     /// The statement of the parameter, absent for the errors of parameters that reference no script
     420              :     pub statement_id: Option<u32>,
     421              :     /// The parameter in the text of the script, with the prefix, in UTF-8 bytes
     422              :     pub text_span: TextSpan,
     423              :     /// The parameter name, empty for a positional parameter
     424              :     pub parameter: String,
     425              :     /// The referenced script, if the name finds one
     426              :     pub script: Option<u32>,
     427              :     /// The referenced attachment, if the name finds one
     428              :     pub attachment: Option<u32>,
     429              :     /// What the reference reads
     430              :     pub kind: ReferenceKind,
     431              :     /// The errors of the reference, the messages of the planner
     432              :     pub errors: Vec<String>,
     433              : }
     434              : 
     435              : /// The checked script references of a script
     436              : #[derive(Clone, Debug, Default, PartialEq, Eq)]
     437              : pub struct CheckedScript {
     438              :     /// Was the script checked? A script whose analysis is not current with its text is not.
     439              :     pub checked: bool,
     440              :     /// The text version of the analysis that was checked
     441              :     pub text_version: u64,
     442              :     /// The table parameters of the script in text order
     443              :     pub references: Vec<CheckedReference>,
     444              : }
     445              : 
     446              : /// Collects the errors of script references by script and text
     447              : #[derive(Default)]
     448              : struct CollectedErrors {
     449              :     errors: BTreeMap<(u32, u32, u32), Vec<String>>,
     450              : }
     451              : 
     452              : impl ReferenceErrors for CollectedErrors {
     453           56 :     fn error(&mut self, script: u32, text_span: Option<TextSpan>, message: String) {
     454           56 :         let span = text_span.unwrap_or_default();
     455           56 :         let messages = self.errors.entry((script, span.offset, span.length)).or_default();
     456           56 :         if !messages.contains(&message) {
     457           56 :             messages.push(message);
     458           56 :         }
     459           56 :     }
     460              : }
     461              : 
     462              : /// Check the script references of every script of a session whose analysis is current with its text, like a plan
     463              : /// of the session would. Other scripts may be broken or stale meanwhile: a referenced script without a current
     464              : /// analysis is read as a single SELECT. Ignores the scope, the order of the scripts and the catalogs.
     465          277 : pub fn check_script_references(request: &SessionPlanRequest) -> Vec<CheckedScript> {
     466          277 :     let count = request.scripts.len();
     467          542 :     let prepared: Vec<Option<Prepared>> = request.scripts.iter().map(|s| Prepared::lenient(s.script)).collect();
     468          277 :     let mut references = ScriptReferences::new(count);
     469          277 :     let mut errors = CollectedErrors::default();
     470          542 :     for (i, p) in prepared.iter().enumerate() {
     471          542 :         if let Some(p) = p {
     472          534 :             references.resolve(&mut errors, request, i as u32, p);
     473          534 :         }
     474              :     }
     475          277 :     references.check(&mut errors, request, &prepared);
     476          277 :     let mut out = Vec::with_capacity(count);
     477          542 :     for (i, p) in prepared.iter().enumerate() {
     478          542 :         let Some(p) = p else {
     479            8 :             out.push(CheckedScript::default());
     480            8 :             continue;
     481              :         };
     482          534 :         let mut checked: Vec<CheckedReference> = references
     483          534 :             .of(i as u32)
     484          534 :             .iter()
     485          534 :             .map(|r| {
     486          202 :                 CheckedReference {
     487          202 :                     statement_id: Some(r.statement_id),
     488          202 :                     text_span: r.text_span,
     489          202 :                     parameter: r.name.clone(),
     490          202 :                     script: r.producer,
     491          202 :                     attachment: r.attachment,
     492          202 :                     kind: r.kind,
     493          202 :                     errors: errors.errors.remove(&(i as u32, r.text_span.offset, r.text_span.length)).unwrap_or_default(),
     494          202 :                 }
     495          202 :             })
     496          534 :             .collect();
     497              :         // Errors of parameters that reference no script, e.g. positional ones
     498          534 :         let rest: Vec<_> = errors.errors.keys().filter(|(s, _, _)| *s == i as u32).copied().collect();
     499          534 :         for key in rest {
     500            4 :             let (_, offset, length) = key;
     501            4 :             checked.push(CheckedReference {
     502            4 :                 statement_id: None,
     503            4 :                 text_span: TextSpan { offset, length },
     504            4 :                 parameter: String::new(),
     505            4 :                 script: None,
     506            4 :                 attachment: None,
     507            4 :                 kind: ReferenceKind::Invalid,
     508            4 :                 errors: errors.errors.remove(&key).unwrap_or_default(),
     509            4 :             });
     510            4 :         }
     511          534 :         checked.sort_by_key(|r| (r.text_span.offset, r.text_span.length));
     512          534 :         out.push(CheckedScript { checked: true, text_version: p.scanned.text_version, references: checked });
     513              :     }
     514          277 :     out
     515          277 : }
     516              : 
     517              : /// Scripts and attachments whose names are equal in case
     518              : #[derive(Clone, Debug, PartialEq, Eq)]
     519              : pub struct TableParameterCollision {
     520              :     /// The name of the first of them
     521              :     pub name: String,
     522              :     /// The scripts with the name
     523              :     pub scripts: Vec<u32>,
     524              :     /// The attachments with the name
     525              :     pub attachments: Vec<u32>,
     526              : }
     527              : 
     528              : /// The names of the scripts and attachments of a session that collide. A table parameter with such a name is
     529              : /// ambiguous, at least in case.
     530          277 : pub fn table_parameter_collisions(request: &SessionPlanRequest) -> Vec<TableParameterCollision> {
     531          277 :     let names: Vec<&str> = table_parameter_names(request).collect();
     532          277 :     colliding_names(names.iter().copied())
     533          277 :         .into_iter()
     534          277 :         .map(|group| {
     535            4 :             let mut collision = TableParameterCollision { name: names[group[0]].to_string(), scripts: Vec::new(), attachments: Vec::new() };
     536            8 :             for i in group {
     537            8 :                 match source_of(request, i) {
     538            6 :                     TableParameterSource::Script(s) => collision.scripts.push(s),
     539            2 :                     TableParameterSource::Attachment(a) => collision.attachments.push(a),
     540              :                 }
     541              :             }
     542            4 :             collision
     543            4 :         })
     544          277 :         .collect()
     545          277 : }
     546              : 
     547              : /// A reference of a spliced statement
     548              : pub(crate) struct SplicedReference {
     549              :     /// The reference
     550              :     pub reference: Reference,
     551              :     /// The name of the common table expression
     552              :     pub cte_name: String,
     553              :     /// The materialization that the reference scans
     554              :     pub materialization: Option<u32>,
     555              : }
     556              : 
     557              : /// A statement with its script references spliced in
     558              : #[derive(Default)]
     559              : pub(crate) struct SplicedStatement {
     560              :     /// The edits of the statement text
     561              :     pub edits: Vec<(TextSpan, String)>,
     562              :     /// The references of the statement
     563              :     pub references: Vec<SplicedReference>,
     564              :     /// The queries of the inlined scripts: (script, statement)
     565              :     pub inlined: Vec<(u32, u32)>,
     566              :     /// The materializations that the statement scans
     567              :     pub inputs: Vec<u32>,
     568              :     /// The attachments that the statement scans, also those that the inlined queries read
     569              :     pub attachments: Vec<u32>,
     570              :     /// The values of inputs that the inlined queries don't read: (script, text span, message)
     571              :     pub input_errors: Vec<(u32, TextSpan, String)>,
     572              : }
     573              : 
     574              : /// The path of an attachment that a statement scans
     575           57 : pub(crate) fn attachment_path(request: &SessionPlanRequest, attachment: u32) -> String {
     576           57 :     format!("{}/{}", request.attachment_prefix, request.attachments[attachment as usize].file)
     577           57 : }
     578              : 
     579              : /// The text of a CTE name or a script name as identifier
     580          451 : fn identifier(name: &str) -> String {
     581          451 :     let quoted = quote_identifier(name);
     582          451 :     if quoted == name && Keyword::find(name.as_bytes()).is_some() {
     583            6 :         return format!("\"{name}\"");
     584          445 :     }
     585          445 :     quoted
     586          451 : }
     587              : 
     588              : /// The relation names that a statement uses unqualified, lower-cased
     589         1094 : fn relation_names(prepared: &Prepared, statement_id: u32, out: &mut BTreeSet<String>) {
     590         1094 :     for table_ref in prepared.analyzed.table_references.iter().filter(|t| t.ast_statement_id == Some(statement_id) && !t.reads_table_parameter()) {
     591          563 :         let Some(relation) = &table_ref.inner else { continue };
     592          563 :         let name = &relation.table_name;
     593          563 :         if name.database_name.text.is_empty() && name.schema_name.text.is_empty() {
     594          563 :             out.insert(name.table_name.text.to_lowercase());
     595          563 :         }
     596              :     }
     597         1094 : }
     598              : 
     599              : /// The common table expressions of a statement: the node, its name lower-cased
     600         1079 : fn ctes<'a>(prepared: &'a Prepared, statement_id: u32) -> impl Iterator<Item = (u32, String)> + 'a {
     601         1079 :     let (scanned, parsed) = (prepared.scanned, prepared.parsed);
     602         1079 :     let statement = &parsed.statements[statement_id as usize];
     603        17422 :     (statement.nodes_begin..statement.nodes_begin + statement.node_count).filter_map(move |ni| {
     604        17422 :         let node = &parsed.nodes[ni as usize];
     605        17422 :         if node.node_type() != NodeType::OBJECT_SQL_CTE {
     606        17375 :             return None;
     607           47 :         }
     608           47 :         let [name] = attrs!(parsed.children(node), [Key::SQL_CTE_NAME]);
     609           47 :         let span = resolve_text_span(&scanned.text, &scanned.symbols, *name?.symbol_span());
     610           47 :         let text = &scanned.text[span.offset as usize..span.end() as usize];
     611           47 :         Some((ni, text.trim_matches('"').to_lowercase()))
     612        17422 :     })
     613         1079 : }
     614              : 
     615              : /// The names of the common table expressions of a statement, lower-cased
     616          742 : fn cte_names(prepared: &Prepared, statement_id: u32, out: &mut BTreeSet<String>) {
     617          742 :     out.extend(ctes(prepared, statement_id).map(|(_, name)| name));
     618          742 : }
     619              : 
     620              : /// The relation names that a statement uses unqualified and the names of its common table expressions,
     621              : /// lower-cased. A common table expression of a script must not shadow them.
     622          352 : fn used_names(prepared: &Prepared, statement_id: u32, out: &mut BTreeSet<String>) {
     623          352 :     relation_names(prepared, statement_id, out);
     624          352 :     cte_names(prepared, statement_id, out);
     625          352 : }
     626              : 
     627              : /// The names of the common table expressions of a statement that the common table expressions of the inlined queries
     628              : /// see at an anchor, lower-cased: those of an enclosing WITH, e.g. the one of an INSERT, and those of the WITH of the
     629              : /// anchor if it is recursive. The common table expressions go before the ones of a WITH that is not recursive.
     630          337 : fn visible_ctes(prepared: &Prepared, statement_id: u32, anchor: u32) -> BTreeSet<String> {
     631          337 :     let nodes = &prepared.parsed.nodes;
     632          337 :     let encloses = |owner: u32| {
     633            5 :         let mut node_id = anchor;
     634              :         loop {
     635            5 :             let parent = nodes[node_id as usize].parent();
     636            5 :             if parent == node_id {
     637            0 :                 return false;
     638            5 :             }
     639            5 :             if parent == owner {
     640            5 :                 return true;
     641            0 :             }
     642            0 :             node_id = parent;
     643              :         }
     644            5 :     };
     645          337 :     let mut out = BTreeSet::new();
     646          337 :     for (cte, name) in ctes(prepared, statement_id) {
     647              :         // The common table expression is an element of the WITH array of its owner
     648           30 :         let owner = nodes[nodes[cte as usize].parent() as usize].parent();
     649           30 :         let visible = if owner == anchor {
     650           25 :             let [recursive] = attrs!(prepared.parsed.children(&nodes[owner as usize]), [Key::SQL_SELECT_WITH_RECURSIVE]);
     651           25 :             recursive.is_some()
     652              :         } else {
     653            5 :             encloses(owner)
     654              :         };
     655           30 :         if visible {
     656           15 :             out.insert(name);
     657           15 :         }
     658              :     }
     659          337 :     out
     660          337 : }
     661              : 
     662              : /// The offset of a query within the parentheses that enclose all of it, `(select ...)` is the query of a subquery.
     663              : /// `(select 1) union (select 2)` is enclosed by no parentheses.
     664          200 : fn inside_parentheses(text: &[u8], span: TextSpan) -> u32 {
     665          200 :     let (mut begin, mut end) = (span.offset as usize, (span.end() as usize).min(text.len()));
     666              :     loop {
     667          214 :         while begin < end && text[begin].is_ascii_whitespace() {
     668            3 :             begin += 1;
     669            3 :         }
     670          214 :         while end > begin && text[end - 1].is_ascii_whitespace() {
     671            3 :             end -= 1;
     672            3 :         }
     673          211 :         if begin >= end || text[begin] != b'(' || text[end - 1] != b')' {
     674          198 :             return begin as u32;
     675           13 :         }
     676              :         // Does the opening parenthesis close at the end? Quoted text and comments do not count.
     677           13 :         let mut depth = 0usize;
     678           13 :         let mut i = begin;
     679           13 :         let mut closes_at_end = false;
     680          177 :         while i < end {
     681          177 :             match text[i] {
     682              :                 b'\'' | b'"' => {
     683            2 :                     let quote = text[i];
     684            2 :                     i += 1;
     685            4 :                     while i < end && text[i] != quote {
     686            2 :                         i += 1;
     687            2 :                     }
     688              :                 }
     689            1 :                 b'-' if text.get(i + 1) == Some(&b'-') => {
     690            5 :                     while i < end && text[i] != b'\n' {
     691            4 :                         i += 1;
     692            4 :                     }
     693              :                 }
     694            2 :                 b'/' if text.get(i + 1) == Some(&b'*') => {
     695            2 :                     i += 2;
     696            8 :                     while i + 1 < end && !(text[i] == b'*' && text[i + 1] == b'/') {
     697            6 :                         i += 1;
     698            6 :                     }
     699            2 :                     i += 1;
     700              :                 }
     701           14 :                 b'(' => depth += 1,
     702              :                 b')' => {
     703           14 :                     depth -= 1;
     704           14 :                     if depth == 0 {
     705           13 :                         closes_at_end = i == end - 1;
     706           13 :                         break;
     707            1 :                     }
     708              :                 }
     709          144 :                 _ => {}
     710              :             }
     711          164 :             i += 1;
     712              :         }
     713           13 :         if !closes_at_end {
     714            2 :             return begin as u32;
     715           11 :         }
     716           11 :         begin += 1;
     717           11 :         end -= 1;
     718              :     }
     719          200 : }
     720              : 
     721              : /// The outermost SELECT around a node within its statement
     722          675 : fn anchor_of(parsed: &dashql_parser::parser::ParsedScript, ast_node_id: u32) -> Option<u32> {
     723          675 :     let mut anchor = None;
     724          675 :     let mut node_id = ast_node_id;
     725              :     loop {
     726         3120 :         let node = &parsed.nodes[node_id as usize];
     727         3120 :         if node.node_type() == NodeType::OBJECT_SQL_SELECT {
     728          734 :             anchor = Some(node_id);
     729         2386 :         }
     730         3120 :         let parent = node.parent();
     731         3120 :         if parent == node_id {
     732          675 :             break;
     733         2445 :         }
     734         2445 :         node_id = parent;
     735              :     }
     736          675 :     anchor
     737          675 : }
     738              : 
     739              : /// Splice the script references of a statement into its text
     740         1054 : pub(crate) fn splice_statement(
     741         1054 :     request: &SessionPlanRequest,
     742         1054 :     prepared: &[Option<Prepared>],
     743         1054 :     references: &ScriptReferences,
     744         1054 :     script: u32,
     745         1054 :     statement_id: u32,
     746         1054 :     materialization_of: &BTreeMap<u32, u32>,
     747         1054 :     materializations: &[ExecutionGraphMaterializationT],
     748         1054 :     inputs: &crate::session_inputs::SessionInputs,
     749         1054 : ) -> SplicedStatement {
     750         1054 :     let mut out = SplicedStatement::default();
     751         1054 :     let direct: Vec<&Reference> = references.of(script).iter().filter(|r| r.statement_id == statement_id && r.kind != ReferenceKind::Invalid).collect();
     752         1054 :     if direct.is_empty() {
     753          855 :         return out;
     754          199 :     }
     755          199 :     let consumer = prepared[script as usize].as_ref().unwrap();
     756          199 :     let (scanned, parsed) = (consumer.scanned, consumer.parsed);
     757              : 
     758              :     // The scripts of the statement, every script after the scripts it references
     759          199 :     let order = references.closure(prepared, direct.iter().copied());
     760              : 
     761              :     // The names of the common table expressions, distinct from the names the statement and the inlined queries use
     762          199 :     let mut used = BTreeSet::new();
     763          199 :     used_names(consumer, statement_id, &mut used);
     764          199 :     for &(producer, kind) in &order {
     765          197 :         if kind == ReferenceKind::Inline {
     766          153 :             let p = prepared[producer as usize].as_ref().unwrap();
     767          153 :             if let Some(terminal) = p.terminal() {
     768          153 :                 used_names(p, terminal, &mut used);
     769          153 :             }
     770           44 :         }
     771              :     }
     772              :     // The attachments that the statement and the inlined queries scan, in the order of their first reference
     773          199 :     let mut attachments: Vec<u32> = Vec::new();
     774              :     {
     775          233 :         let mut read = |r: &Reference| {
     776          233 :             if let (ReferenceKind::Attachment, Some(a)) = (r.kind, r.attachment) {
     777           24 :                 if !attachments.contains(&a) {
     778           23 :                     attachments.push(a);
     779           23 :                 }
     780          209 :             }
     781          233 :         };
     782          213 :         direct.iter().for_each(|r| read(r));
     783          199 :         for &(producer, kind) in &order {
     784          197 :             let terminal = prepared[producer as usize].as_ref().and_then(Prepared::terminal);
     785          197 :             if kind == ReferenceKind::Inline && terminal.is_some() {
     786          153 :                 references.of(producer).iter().filter(|r| Some(r.statement_id) == terminal).for_each(&mut read);
     787           44 :             }
     788              :         }
     789              :     }
     790              :     // What the common table expressions stand for: the attachments first, the inlined queries may read them, then the
     791              :     // scripts, every script after the scripts it references
     792          199 :     let sources: Vec<TableParameterSource> =
     793          199 :         attachments.iter().map(|&a| TableParameterSource::Attachment(a)).chain(order.iter().map(|&(p, _)| TableParameterSource::Script(p))).collect();
     794          451 :     let name_of = |source: TableParameterSource| match source {
     795          404 :         TableParameterSource::Script(script) => request.scripts[script as usize].name,
     796           47 :         TableParameterSource::Attachment(attachment) => request.attachments[attachment as usize].name,
     797          451 :     };
     798              : 
     799              :     // A table parameter is no relation of the statement, a relation of the same name as a script would be shadowed
     800          199 :     let mut cte_names: BTreeMap<TableParameterSource, String> = BTreeMap::new();
     801          199 :     let mut taken = used;
     802          220 :     for &source in &sources {
     803          220 :         let name = name_of(source);
     804          229 :         let shadows = |candidate: &str| taken.contains(&candidate.to_lowercase());
     805          220 :         let mut candidate = name.to_string();
     806          220 :         if shadows(&candidate) {
     807            9 :             let mut n = 2;
     808              :             loop {
     809            9 :                 candidate = format!("{name}_{n}");
     810            9 :                 if !shadows(&candidate) {
     811            9 :                     break;
     812            0 :                 }
     813            0 :                 n += 1;
     814              :             }
     815          211 :         }
     816          220 :         taken.insert(candidate.to_lowercase());
     817          220 :         cte_names.insert(source, candidate);
     818              :     }
     819              : 
     820              :     // The edits of the references of a statement text: the parameter becomes the name of the common table expression,
     821              :     // a renamed one keeps the name of the script as alias unless the statement reads a relation of that name
     822          352 :     let reference_edits = |p: &Prepared, statement: u32, refs: &[Reference]| -> Vec<(TextSpan, String)> {
     823          352 :         let mut edits = Vec::new();
     824          352 :         let mut relations = BTreeSet::new();
     825          352 :         relation_names(p, statement, &mut relations);
     826          352 :         for r in refs.iter().filter(|r| r.statement_id == statement && r.kind != ReferenceKind::Invalid) {
     827          231 :             let Some(source) = r.source() else { continue };
     828          231 :             let cte = &cte_names[&source];
     829          231 :             let tableref = &p.parsed.nodes[p.parsed.nodes[r.ast_node_id as usize].parent() as usize];
     830          231 :             let [alias] = attrs!(p.parsed.children(tableref), [Key::SQL_TABLEREF_ALIAS]);
     831          231 :             let has_alias = alias.is_some_and(|a| a.node_type() != NodeType::NONE);
     832          231 :             let script_name = name_of(source);
     833          231 :             let keeps_name = has_alias || cte == script_name || relations.contains(&script_name.to_lowercase());
     834          231 :             let text = if keeps_name { identifier(cte) } else { format!("{} as {}", identifier(cte), identifier(script_name)) };
     835          231 :             edits.push((r.text_span, text));
     836              :         }
     837          352 :         edits
     838          352 :     };
     839              : 
     840              :     // The common table expressions in order
     841          199 :     let mut ctes: Vec<String> = Vec::new();
     842          199 :     for &attachment in &attachments {
     843           23 :         let cte = identifier(&cte_names[&TableParameterSource::Attachment(attachment)]);
     844           23 :         let path = attachment_path(request, attachment);
     845           23 :         ctes.push(format!("{cte} as (select * from external('{}', format => 'parquet'))", path.replace('\'', "''")));
     846           23 :     }
     847          199 :     out.attachments = attachments;
     848          199 :     for &(producer, kind) in &order {
     849          197 :         let cte = identifier(&cte_names[&TableParameterSource::Script(producer)]);
     850          197 :         match kind {
     851              :             ReferenceKind::Inline => {
     852          153 :                 let p = prepared[producer as usize].as_ref().unwrap();
     853          153 :                 let Some(terminal) = p.terminal() else { continue };
     854          153 :                 let span = p.parsed.associate_descriptions()[terminal as usize].statement_span;
     855          153 :                 let mut edits = reference_edits(p, terminal, references.of(producer));
     856              :                 // The inlined query binds the inputs of its script
     857              :                 // Within the connection of the statement
     858          153 :                 let dialect = request.connections[request.scripts[script as usize].connection as usize].dialect;
     859          153 :                 let bindings = crate::session_inputs::bind_inputs(inputs, p, Some(terminal), dialect);
     860          153 :                 edits.extend(bindings.edits);
     861          153 :                 out.input_errors.extend(bindings.errors.into_iter().map(|(span, message)| (producer, span, message)));
     862          153 :                 let body = read_executable_text_with(p.scanned, span, &edits);
     863          153 :                 ctes.push(format!("{cte} as (\n{}\n)", body.trim_end()));
     864          153 :                 out.inlined.push((producer, terminal));
     865              :             }
     866              :             _ => {
     867           44 :                 let Some(&m) = materialization_of.get(&producer) else { continue };
     868           44 :                 let path = materializations[m as usize].path.as_deref().unwrap_or("");
     869           44 :                 ctes.push(format!("{cte} as (select * from external('{}', format => 'parquet'))", path.replace('\'', "''")));
     870           44 :                 if !out.inputs.contains(&m) {
     871           44 :                     out.inputs.push(m);
     872           44 :                 }
     873              :             }
     874              :         }
     875              :     }
     876              : 
     877              :     // The common table expressions go into the outermost SELECT around the references, before the ones it has
     878          199 :     let mut anchors: BTreeMap<u32, Vec<&Reference>> = BTreeMap::new();
     879          213 :     for r in &direct {
     880          213 :         if let Some(anchor) = anchor_of(parsed, r.ast_node_id) {
     881          213 :             anchors.entry(anchor).or_default().push(r);
     882          213 :         }
     883              :     }
     884          199 :     for &anchor in anchors.keys() {
     885          199 :         let node = &parsed.nodes[anchor as usize];
     886          199 :         let [with] = attrs!(parsed.children(node), [Key::SQL_SELECT_WITH_CTES]);
     887          199 :         let first_cte = with.filter(|w| w.node_type() == NodeType::ARRAY && w.children_count() > 0).map(|w| parsed.children(w)[0]);
     888          199 :         match first_cte {
     889            9 :             Some(first) => {
     890            9 :                 let at = resolve_text_span(&scanned.text, &scanned.symbols, *first.symbol_span()).offset;
     891            9 :                 out.edits.push((TextSpan::new(at, 0), format!("{},\n", ctes.join(",\n"))));
     892            9 :             }
     893          190 :             None => {
     894          190 :                 let span = resolve_text_span(&scanned.text, &scanned.symbols, *node.symbol_span());
     895          190 :                 let at = inside_parentheses(scanned.text.as_bytes(), span);
     896          190 :                 out.edits.push((TextSpan::new(at, 0), format!("with {}\n", ctes.join(",\n"))));
     897          190 :             }
     898              :         }
     899              :     }
     900          199 :     out.edits.extend(reference_edits(consumer, statement_id, references.of(script)));
     901          426 :     out.edits.sort_by_key(|(s, _)| (s.offset, s.length));
     902          213 :     for r in direct {
     903          213 :         let Some(source) = r.source() else { continue };
     904          213 :         out.references.push(SplicedReference {
     905          213 :             reference: r.clone(),
     906          213 :             cte_name: cte_names[&source].clone(),
     907          213 :             materialization: match (r.kind, r.producer) {
     908           41 :                 (ReferenceKind::Scan, Some(producer)) => materialization_of.get(&producer).copied(),
     909          172 :                 _ => None,
     910              :             },
     911              :         });
     912              :     }
     913          199 :     out
     914         1054 : }
     915              : 
     916              : #[cfg(test)]
     917              : mod tests {
     918              :     use super::*;
     919              : 
     920              :     /// The text from the offset of the query inside the parentheses that enclose all of it
     921           10 :     fn inside(text: &str) -> &str {
     922           10 :         let span = TextSpan { offset: 0, length: text.len() as u32 };
     923           10 :         &text[inside_parentheses(text.as_bytes(), span) as usize..]
     924           10 :     }
     925              : 
     926              :     #[test]
     927            1 :     fn skips_the_parentheses_that_enclose_all_of_a_query() {
     928            1 :         assert_eq!(inside("select 1"), "select 1");
     929            1 :         assert_eq!(inside("  (select 1) "), "select 1) ");
     930            1 :         assert_eq!(inside("(( select 1 ))"), "select 1 ))");
     931              :         // The first parenthesis closes before the end
     932            1 :         assert_eq!(inside("(select 1) union (select 2)"), "(select 1) union (select 2)");
     933            1 :         assert_eq!(inside("()"), ")");
     934            1 :     }
     935              : 
     936              :     #[test]
     937            1 :     fn parentheses_in_quotes_and_comments_do_not_count() {
     938            1 :         assert_eq!(inside("(select ')' as a)"), "select ')' as a)");
     939            1 :         assert_eq!(inside(r#"(select 1 as ")")"#), r#"select 1 as ")")"#);
     940            1 :         assert_eq!(inside("(select 1 -- )\n)"), "select 1 -- )\n)");
     941            1 :         assert_eq!(inside("(select /* ) */ 1)"), "select /* ) */ 1)");
     942            1 :         assert_eq!(inside("(select 1 /* ) */) union (select 2)"), "(select 1 /* ) */) union (select 2)");
     943            1 :     }
     944              : }
        

Generated by: LCOV version 2.0-1