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

            Line data    Source code
       1              : //! Batch processing: scan, parse and analyze many scripts against shared
       2              : //! descriptor context in one call.
       3              : 
       4              : use dashql_core::catalog::{Catalog, CatalogLayer};
       5              : use dashql_core::error::{Error, Result};
       6              : use dashql_core::pack::{pack_analyzed_script, pack_catalog_descriptor, pack_parsed_script, pack_scanned_script};
       7              : use crate::visualize::pack_visualization;
       8              : use dashql_core::script::Script;
       9              : use dashql_buffers::analyzer::AnalyzerErrorSeverity;
      10              : use dashql_buffers::batch::{BatchDiagnosticT, BatchRequest, BatchResultT, BatchScriptResultT};
      11              : pub use dashql_buffers::batch::{DiagnosticSeverity, ProcessingStage};
      12              : use dashql_parser::parser::context::resolve_text_span;
      13              : use dashql_parser::span::TextSpan;
      14              : 
      15              : /// Pack an object API value into FlatBuffer bytes
      16              : macro_rules! pack_bytes {
      17              :     ($value:expr) => {{
      18              :         let mut builder = flatbuffers::FlatBufferBuilder::new();
      19              :         let packed = $value.pack(&mut builder);
      20              :         builder.finish(packed, None);
      21              :         builder.finished_data().to_vec()
      22              :     }};
      23              : }
      24              : 
      25              : /// A vector that is left out of the packed buffer when empty
      26           74 : fn non_empty<T>(v: Vec<T>) -> Option<Vec<T>> {
      27           74 :     (!v.is_empty()).then_some(v)
      28           74 : }
      29              : 
      30              : /// A diagnostic of a checked script
      31              : #[derive(Clone, Debug, PartialEq, Eq)]
      32              : pub struct Diagnostic {
      33              :     /// The processing stage that reported the diagnostic
      34              :     pub stage: ProcessingStage,
      35              :     /// Whether the diagnostic is an error or a warning
      36              :     pub severity: DiagnosticSeverity,
      37              :     /// The UTF-8 byte span in the script text
      38              :     pub span: TextSpan,
      39              :     /// The diagnostic message
      40              :     pub message: String,
      41              : }
      42              : 
      43              : /// A script processed by [`check_script`]
      44              : pub struct CheckedScript {
      45              :     /// The processed script, not published to the catalog
      46              :     pub script: Script,
      47              :     /// The diagnostics of all stages that ran, in stage order
      48              :     pub diagnostics: Vec<Diagnostic>,
      49              :     /// Why the analysis failed, the diagnostics of the earlier stages are kept
      50              :     pub failure: Option<String>,
      51              : }
      52              : 
      53              : impl CheckedScript {
      54              :     /// Whether processing failed or found an error
      55           17 :     pub fn has_errors(&self) -> bool {
      56           17 :         self.failure.is_some() || self.diagnostics.iter().any(|d| d.severity == DiagnosticSeverity::ERROR)
      57           17 :     }
      58              : }
      59              : 
      60              : /// Scan, parse and analyze a script against a catalog up to `until`, collecting the diagnostics of every
      61              : /// stage. The script is not published to the catalog.
      62           52 : pub fn check_script(catalog: &Catalog, text: &str, until: ProcessingStage) -> CheckedScript {
      63           52 :     let mut diagnostics = Vec::new();
      64           52 :     let mut diagnostic = |stage, severity, span, message: &str| {
      65           22 :         diagnostics.push(Diagnostic { stage, severity, span, message: message.to_string() });
      66           22 :     };
      67           52 :     let mut script = Script::new(catalog);
      68           52 :     script.replace_text(text);
      69           52 :     script.scan();
      70           52 :     for e in &script.scanned.as_ref().unwrap().errors {
      71            3 :         diagnostic(ProcessingStage::SCAN, DiagnosticSeverity::ERROR, e.location, &e.message);
      72            3 :     }
      73           52 :     if until >= ProcessingStage::PARSE {
      74           46 :         script.parse();
      75           46 :         let parsed = script.parsed.as_ref().unwrap();
      76           46 :         for e in &parsed.errors {
      77           10 :             let span = resolve_text_span(&parsed.scanned.text, &parsed.scanned.symbols, e.location);
      78           10 :             diagnostic(ProcessingStage::PARSE, DiagnosticSeverity::ERROR, span, &e.message);
      79           10 :         }
      80            6 :     }
      81           52 :     let mut failure = None;
      82           52 :     if until >= ProcessingStage::ANALYZE {
      83           43 :         match script.analyze(catalog, false) {
      84              :             Ok(()) => {
      85           43 :                 let analyzed = script.analyzed.as_ref().unwrap();
      86           43 :                 let scanned = &analyzed.parsed.scanned;
      87           43 :                 for e in &analyzed.errors {
      88            9 :                     let severity = if e.severity == AnalyzerErrorSeverity::WARNING {
      89            3 :                         DiagnosticSeverity::WARNING
      90              :                     } else {
      91            6 :                         DiagnosticSeverity::ERROR
      92              :                     };
      93            9 :                     let span = match (e.text_span, e.symbol_span) {
      94            9 :                         (Some(t), _) => t,
      95            0 :                         (None, Some(s)) => resolve_text_span(&scanned.text, &scanned.symbols, s),
      96            0 :                         (None, None) => TextSpan::default(),
      97              :                     };
      98            9 :                     diagnostic(ProcessingStage::ANALYZE, severity, span, &e.message);
      99              :                 }
     100              :             }
     101            0 :             Err(e) => failure = Some(e.message),
     102              :         }
     103            9 :     }
     104           52 :     CheckedScript { script, diagnostics, failure }
     105           52 : }
     106              : 
     107              : /// Process a batch request. Inputs share only the descriptor context, they never
     108              : /// publish declarations to each other.
     109           29 : pub fn process_batch(request: &[u8]) -> Result<Vec<u8>> {
     110           29 :     let request = if request.is_empty() { None } else { flatbuffers::root::<BatchRequest>(request).ok() };
     111           29 :     let request = request.ok_or_else(|| Error::invalid_argument("invalid BatchRequest FlatBuffer"))?;
     112           26 :     let mut catalog = Catalog::new();
     113           26 :     if let Some(descriptors) = request.catalog_descriptors() {
     114              :         // Earlier descriptors get older entry ids and win on name conflicts
     115           26 :         for descriptor in descriptors.iter() {
     116            7 :             let data = descriptor.data().ok_or_else(|| Error::invalid_argument("missing batch catalog descriptor bytes"))?;
     117            7 :             let id = catalog.allocate_entry_id();
     118            7 :             catalog.replace_descriptor(id, CatalogLayer::CONNECTION, data.bytes())?;
     119              :         }
     120            0 :     }
     121              : 
     122           24 :     let mut result = BatchResultT::default();
     123           24 :     let mut scripts = Vec::new();
     124           26 :     for input in request.scripts().into_iter().flatten() {
     125           26 :         let mut out = BatchScriptResultT::default();
     126           26 :         out.id = input.id().filter(|id| !id.is_empty()).map(str::to_string);
     127           26 :         let outputs = input.outputs();
     128           26 :         if outputs & !15 != 0 {
     129            1 :             out.failure = Some("invalid batch output mask".into());
     130           25 :         } else if outputs != 0 {
     131           24 :             let until = if outputs & 12 != 0 {
     132           17 :                 ProcessingStage::ANALYZE
     133            7 :             } else if outputs & 2 != 0 {
     134            2 :                 ProcessingStage::PARSE
     135              :             } else {
     136            5 :                 ProcessingStage::SCAN
     137              :             };
     138              :             // The FlatBuffers verifier already rejects the whole request if a text is not valid UTF-8
     139           24 :             let checked = check_script(&catalog, input.text().unwrap_or(""), until);
     140           24 :             let script = &checked.script;
     141           24 :             if outputs & 1 != 0 {
     142            5 :                 out.scanned = script.scanned.as_deref().map(|s| pack_bytes!(pack_scanned_script(s)));
     143           19 :             }
     144           24 :             if outputs & 2 != 0 {
     145            3 :                 out.parsed = script.parsed.as_deref().map(|p| pack_bytes!(pack_parsed_script(p)));
     146           21 :             }
     147           24 :             out.failure = checked.failure.clone();
     148           24 :             if let (None, Some(analyzed)) = (&checked.failure, script.analyzed.as_deref()) {
     149           17 :                 if outputs & 4 != 0 {
     150           13 :                     out.analyzed = Some(pack_bytes!(pack_analyzed_script(analyzed, &pack_visualization)));
     151           13 :                 }
     152           17 :                 if outputs & 8 != 0 && !checked.has_errors() {
     153            5 :                     match pack_catalog_descriptor(&analyzed.entry) {
     154            5 :                         Ok(descriptor) => out.catalog_descriptor = Some(descriptor),
     155            0 :                         Err(e) => out.failure = Some(e.message),
     156              :                     }
     157           12 :                 }
     158            7 :             }
     159           24 :             let diagnostics = checked.diagnostics.into_iter().map(|d| {
     160            8 :                 let mut out = BatchDiagnosticT::default();
     161            8 :                 out.stage = d.stage;
     162            8 :                 out.severity = d.severity;
     163            8 :                 out.offset = d.span.offset;
     164            8 :                 out.length = d.span.length;
     165            8 :                 out.message = (!d.message.is_empty()).then_some(d.message);
     166            8 :                 out
     167            8 :             });
     168           24 :             out.diagnostics = non_empty(diagnostics.collect());
     169            1 :         }
     170              :         // The object API leaves out empty vectors
     171           26 :         out.scanned = out.scanned.and_then(non_empty);
     172           26 :         out.parsed = out.parsed.and_then(non_empty);
     173           26 :         out.analyzed = out.analyzed.and_then(non_empty);
     174           26 :         out.catalog_descriptor = out.catalog_descriptor.and_then(non_empty);
     175           26 :         scripts.push(out);
     176              :     }
     177           24 :     result.scripts = non_empty(scripts);
     178           24 :     let mut builder = flatbuffers::FlatBufferBuilder::new();
     179           24 :     let packed = result.pack(&mut builder);
     180           24 :     builder.finish(packed, None);
     181           24 :     Ok(builder.finished_data().to_vec())
     182           29 : }
        

Generated by: LCOV version 2.0-1