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

            Line data    Source code
       1              : //! The dataflow of a session: which statements read the data that other statements produce.
       2              : //!
       3              : //! The dataflow view draws every script as a box of its statements and the flow of data between the
       4              : //! statements as edges. The dataflow is a projection of a tolerant plan of the session (`PlanScope::Tolerant`),
       5              : //! so relations and script references are resolved exactly like a run resolves them:
       6              : //!
       7              : //! - A table parameter `:foo` reads the result of the terminal query of `foo.sql`, inlined within one connection
       8              : //!   or scanned as materialized result on the LOCAL connection. Invalid references are edges too, as long as their
       9              : //!   name finds a script, the editor reports their errors.
      10              : //! - A statement reads or writes a relation after another statement created or wrote it: the planner edges
      11              : //!   `READ_AFTER_WRITE`, `WRITE_AFTER_WRITE` and `DDL` of a relation whose source statement wrote it. A drop
      12              : //!   reads no data. A statement that reads a relation only through the query of an inlined script reads it through
      13              : //!   that script, the edge into the statement is left out.
      14              : //!
      15              : //! - A statement uses an input, a named scalar parameter `:min` (`session_inputs`). The inputs are nodes of their own,
      16              : //!   an input edge starts at the input and ends at every statement that uses it.
      17              : //! - A statement scans an attachment of the session with a table parameter `:sales`. The attachments are nodes of
      18              : //!   their own, an attachment edge starts at the attachment and ends at the statement of the parameter.
      19              : //!
      20              : //! The planner edges that only order statements (`WRITE_AFTER_READ`, `BARRIER`, `SEQUENCE`) and the edges of
      21              : //! materializations, which the references stand for, are no dataflow.
      22              : 
      23              : use crate::planner::{plan, runs_on_request, PlanScope, Prepared, SessionPlanRequest};
      24              : use crate::script_references::ReferenceKind;
      25              : use crate::session_inputs::SessionInput;
      26              : use dashql_buffers::execution::{ExecutionGraphAccessMode as Mode, ExecutionGraphEdgeReason as Reason, ExecutionGraphNodeT, ExecutionGraphT};
      27              : use dashql_buffers::parser::StatementType;
      28              : use dashql_parser::span::TextSpan;
      29              : use std::collections::{BTreeMap, BTreeSet};
      30              : 
      31              : const NONE: u32 = u32::MAX;
      32              : 
      33              : /// What carries the data of an edge
      34              : #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
      35              : pub enum DataflowEdgeKind {
      36              :     /// A table parameter inlines the terminal query of the referenced script
      37              :     InlineReference,
      38              :     /// A table parameter scans the materialized result of the referenced script
      39              :     ScanReference,
      40              :     /// A table parameter with errors
      41              :     InvalidReference,
      42              :     /// A statement reads or writes a relation that another statement created or wrote
      43              :     Table,
      44              :     /// A statement uses an input, the script of the source port is the index of the input
      45              :     Input,
      46              :     /// A statement scans an attachment, the script of the source port is the index of the attachment
      47              :     Attachment,
      48              : }
      49              : 
      50              : /// An attachment of the session
      51              : #[derive(Clone, Debug, PartialEq, Eq)]
      52              : pub struct DataflowAttachment {
      53              :     /// The name of the table parameter, `sales` for `sales.parquet`
      54              :     pub name: String,
      55              :     /// The file name
      56              :     pub file: String,
      57              : }
      58              : 
      59              : /// A statement of a script
      60              : #[derive(Clone, Debug, PartialEq, Eq)]
      61              : pub struct DataflowStatement {
      62              :     /// The statement type
      63              :     pub statement_type: StatementType,
      64              :     /// The relation that the statement creates, writes or drops
      65              :     pub target: Option<String>,
      66              :     /// The text of the statement, in UTF-8 bytes
      67              :     pub text_span: TextSpan,
      68              : }
      69              : 
      70              : /// A script of the session
      71              : #[derive(Clone, Debug, Default, PartialEq, Eq)]
      72              : pub struct DataflowScript {
      73              :     /// Was the script planned? A script without a current analysis is not, its statements and the edges that start
      74              :     /// or end at it are unknown.
      75              :     pub analyzed: bool,
      76              :     /// The text version of the analysis
      77              :     pub text_version: u64,
      78              :     /// The statements in script order
      79              :     pub statements: Vec<DataflowStatement>,
      80              :     /// Does the script run only on request, its `@manual` tag?
      81              :     pub manual: bool,
      82              : }
      83              : 
      84              : /// An end of an edge: a statement of a script
      85              : #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
      86              : pub struct DataflowPort {
      87              :     /// The script index
      88              :     pub script: u32,
      89              :     /// The statement id
      90              :     pub statement: u32,
      91              : }
      92              : 
      93              : /// The flow of data from one statement to another
      94              : #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
      95              : pub struct DataflowEdge {
      96              :     /// The statement that produces the data
      97              :     pub from: DataflowPort,
      98              :     /// The statement that reads the data
      99              :     pub to: DataflowPort,
     100              :     /// What carries the data
     101              :     pub kind: DataflowEdgeKind,
     102              :     /// The parameter name of a reference, the relation name of a table edge
     103              :     pub name: String,
     104              : }
     105              : 
     106              : /// The dataflow of a session
     107              : #[derive(Clone, Debug, Default, PartialEq)]
     108              : pub struct Dataflow {
     109              :     /// The scripts in session order
     110              :     pub scripts: Vec<DataflowScript>,
     111              :     /// The inputs in the order of their first use
     112              :     pub inputs: Vec<SessionInput>,
     113              :     /// The attachments of the request, the sources of the attachment edges
     114              :     pub attachments: Vec<DataflowAttachment>,
     115              :     /// The edges, sorted and without duplicates
     116              :     pub edges: Vec<DataflowEdge>,
     117              : }
     118              : 
     119              : /// A relation that a node accesses: an entity or an unresolved reference
     120              : type Relation = (u32, u32);
     121              : 
     122          159 : fn relation_of(entity: u32, unresolved: u32) -> Option<Relation> {
     123          159 :     (entity != NONE || unresolved != NONE).then_some((entity, unresolved))
     124          159 : }
     125              : 
     126              : /// The name of a relation of the graph
     127           95 : fn relation_name(graph: &ExecutionGraphT, (entity, unresolved): Relation) -> String {
     128           95 :     let name = if entity != NONE {
     129           95 :         graph.entities.as_ref().and_then(|e| e.get(entity as usize)).and_then(|e| e.name.clone())
     130              :     } else {
     131            0 :         graph.unresolved_references.as_ref().and_then(|r| r.get(unresolved as usize)).and_then(|r| r.name.clone())
     132              :     };
     133           95 :     name.unwrap_or_default()
     134           95 : }
     135              : 
     136              : /// The relations that a node writes or creates
     137          186 : fn writes(node: &ExecutionGraphNodeT) -> impl Iterator<Item = Relation> + '_ {
     138          186 :     node.accesses
     139          186 :         .iter()
     140          186 :         .flatten()
     141          186 :         .filter(|a| a.mode != Mode::READ)
     142          186 :         .filter_map(|a| relation_of(a.entity, a.unresolved_reference))
     143          186 : }
     144              : 
     145              : /// Derive the dataflow of the scripts of a session. Ignores the scope and the materializations of the request.
     146           41 : pub fn derive_session_dataflow(request: &SessionPlanRequest) -> Dataflow {
     147           41 :     let tolerant = SessionPlanRequest { scope: PlanScope::Tolerant, materialized: &[], ..*request };
     148           41 :     let plan = plan(&tolerant);
     149           41 :     let graph = &plan.graph;
     150           41 :     let empty = Vec::new();
     151           41 :     let nodes = graph.nodes.as_ref().unwrap_or(&empty);
     152          132 :     let port = |node: &ExecutionGraphNodeT| DataflowPort { script: node.script, statement: node.statement_id };
     153          130 :     let node_of: BTreeMap<(u32, u32), u32> = nodes.iter().enumerate().map(|(i, n)| ((n.script, n.statement_id), i as u32)).collect();
     154              : 
     155              :     // The statements, from the nodes of the plan
     156           41 :     let mut scripts: Vec<DataflowScript> = vec![DataflowScript::default(); request.scripts.len()];
     157          130 :     for (node, &text_span) in nodes.iter().zip(&plan.spans) {
     158          130 :         let Some(script) = scripts.get_mut(node.script as usize) else { continue };
     159          130 :         script.analyzed = true;
     160          130 :         script.statements.push(DataflowStatement {
     161          130 :             statement_type: node.statement_type,
     162          130 :             target: writes(node).next().map(|r| relation_name(graph, r)),
     163          130 :             text_span,
     164              :         });
     165              :     }
     166          113 :     for (i, script) in scripts.iter_mut().enumerate() {
     167          113 :         let input = request.scripts[i].script;
     168              :         // A script without statements has no nodes, it is planned if its analysis is current
     169          113 :         if !script.analyzed {
     170            3 :             script.analyzed = Prepared::lenient(input).is_some_and(|p| p.parsed.statements.is_empty());
     171          110 :         }
     172          113 :         if script.analyzed {
     173          110 :             script.text_version = input.analyzed.as_ref().map_or(0, |a| a.parsed.scanned.text_version);
     174          110 :             script.manual = runs_on_request(input);
     175            3 :         }
     176              :     }
     177              : 
     178           41 :     let mut edges: BTreeSet<DataflowEdge> = BTreeSet::new();
     179              :     // The terminal node of a script
     180           41 :     let terminal_of = |script: u32| -> Option<u32> {
     181           28 :         let meta = graph.scripts.as_ref()?.get(script as usize)?;
     182           28 :         node_of.get(&(script, meta.terminal_statement_id)).copied()
     183           28 :     };
     184              : 
     185              :     // Script references and attachments
     186           41 :     for reference in graph.references.iter().flatten() {
     187           18 :         if reference.attachment != NONE {
     188            2 :             let Some(to) = nodes.get(reference.node as usize) else { continue };
     189            2 :             edges.insert(DataflowEdge {
     190            2 :                 from: DataflowPort { script: reference.attachment, statement: 0 },
     191            2 :                 to: port(to),
     192            2 :                 kind: DataflowEdgeKind::Attachment,
     193            2 :                 name: reference.parameter.clone().unwrap_or_default(),
     194            2 :             });
     195            2 :             continue;
     196           16 :         }
     197           16 :         let (Some(from), Some(to)) = (terminal_of(reference.script), nodes.get(reference.node as usize)) else { continue };
     198           16 :         edges.insert(DataflowEdge {
     199           16 :             from: port(&nodes[from as usize]),
     200           16 :             to: port(to),
     201           16 :             kind: if reference.materialization != NONE { DataflowEdgeKind::ScanReference } else { DataflowEdgeKind::InlineReference },
     202           16 :             name: reference.parameter.clone().unwrap_or_default(),
     203              :         });
     204              :     }
     205          113 :     for consumer in 0..request.scripts.len() as u32 {
     206          113 :         for reference in plan.references.of(consumer).iter().filter(|r| r.kind == ReferenceKind::Invalid) {
     207            6 :             let Some(from) = reference.producer.and_then(terminal_of) else { continue };
     208            4 :             if !node_of.contains_key(&(consumer, reference.statement_id)) {
     209            0 :                 continue;
     210            4 :             }
     211            4 :             edges.insert(DataflowEdge {
     212            4 :                 from: port(&nodes[from as usize]),
     213            4 :                 to: DataflowPort { script: consumer, statement: reference.statement_id },
     214            4 :                 kind: DataflowEdgeKind::InvalidReference,
     215            4 :                 name: reference.name.clone(),
     216            4 :             });
     217              :         }
     218              :     }
     219              : 
     220              :     // Relations: the edges whose source wrote the relation
     221           41 :     let mut table_edges: BTreeMap<(u32, Relation), BTreeSet<u32>> = BTreeMap::new();
     222           87 :     for e in graph.edges.iter().flatten() {
     223           87 :         if !matches!(e.reason, Reason::READ_AFTER_WRITE | Reason::WRITE_AFTER_WRITE | Reason::DDL) {
     224           31 :             continue;
     225           56 :         }
     226           56 :         let Some(relation) = relation_of(e.entity, e.unresolved_reference) else { continue };
     227           56 :         let (from, to) = (&nodes[e.from as usize], &nodes[e.to as usize]);
     228              :         // A drop neither produces nor reads data
     229          107 :         let drop = |node: &ExecutionGraphNodeT| matches!(node.statement_type, StatementType::DROP_TABLE | StatementType::DROP_VIEW);
     230           56 :         if !writes(from).any(|r| r == relation) || drop(from) || drop(to) {
     231            5 :             continue;
     232           51 :         }
     233           51 :         table_edges.entry((e.from, relation)).or_default().insert(e.to);
     234              :     }
     235           41 :     for (&(from, relation), targets) in &table_edges {
     236           51 :         for &to in targets {
     237              :             // A read only through the query of an inlined script flows through that script
     238           51 :             let node = &nodes[to as usize];
     239           51 :             let through_inlined = node
     240           51 :                 .inlined_scripts
     241           51 :                 .iter()
     242           51 :                 .flatten()
     243           51 :                 .filter_map(|&script| terminal_of(script))
     244           51 :                 .any(|terminal| terminal != to && targets.contains(&terminal));
     245           51 :             if through_inlined && plan.inherited.contains(&(to, relation.0, relation.1)) {
     246            4 :                 continue;
     247           47 :             }
     248           47 :             edges.insert(DataflowEdge {
     249           47 :                 from: port(&nodes[from as usize]),
     250           47 :                 to: port(&nodes[to as usize]),
     251           47 :                 kind: DataflowEdgeKind::Table,
     252           47 :                 name: relation_name(graph, relation),
     253           47 :             });
     254              :         }
     255              :     }
     256              : 
     257              :     // Inputs
     258           41 :     for (i, input) in plan.inputs.inputs.iter().enumerate() {
     259           10 :         for consumer in input.consumers.iter().filter(|c| node_of.contains_key(&(c.script, c.statement))) {
     260           10 :             edges.insert(DataflowEdge {
     261           10 :                 from: DataflowPort { script: i as u32, statement: 0 },
     262           10 :                 to: DataflowPort { script: consumer.script, statement: consumer.statement },
     263           10 :                 kind: DataflowEdgeKind::Input,
     264           10 :                 name: input.name.to_string(),
     265           10 :             });
     266           10 :         }
     267              :     }
     268              : 
     269           41 :     let attachments = request.attachments.iter().map(|a| DataflowAttachment { name: a.name.to_string(), file: a.file.to_string() }).collect();
     270           41 :     Dataflow { scripts, inputs: plan.inputs.inputs, attachments, edges: edges.into_iter().collect() }
     271           41 : }
        

Generated by: LCOV version 2.0-1