LCOV - code coverage report
Current view: top level - dashql-core/src - script.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 99.1 % 333 330
Test Date: 2026-11-06 17:46:50

            Line data    Source code
       1              : //! A script: the text of an editor or a catalog entry and its latest scanned, parsed and analyzed
       2              : //! versions.
       3              : //!
       4              : //! The text is a rope indexed by Unicode code points, so that edits in the middle of large scripts
       5              : //! stay logarithmic.
       6              : //!
       7              : //! A script does not hold a reference to its catalog. Operations that read
       8              : //! the catalog take it as an argument, and dropping a script does not modify the catalog: the owner
       9              : //! drops the script from the catalog explicitly (`Catalog::drop_script`).
      10              : 
      11              : use crate::analyzer::{analyze_with, AnalyzedScript};
      12              : use crate::table_parameters::TableParameters;
      13              : use crate::catalog::{Catalog, CatalogEntryId};
      14              : use crate::completion::Completion;
      15              : use crate::cursor::{ScriptCursor, ScriptView};
      16              : use crate::error::{Error, Result};
      17              : use crate::formatter::Formatter;
      18              : use crate::vis_rewrite::{VisBoard, VisFlow, VisRenderer};
      19              : use dashql_buffers::formatting::FormattingConfigT;
      20              : use dashql_buffers::parser::{Node, StatementType};
      21              : use dashql_buffers::statistics::{ScriptMemoryStatisticsT, ScriptProcessingTimingsT, ScriptStatisticsT};
      22              : use dashql_buffers::status::StatusCode;
      23              : use dashql_parser::parser::{parse, ParsedScript};
      24              : use dashql_parser::scanner::{scan, ScannedScript};
      25              : use dashql_parser::span::TextSpan;
      26              : use ropey::Rope;
      27              : use std::sync::Arc;
      28              : 
      29              : /// The version of the script text, bumped by every edit
      30              : pub type TextVersion = u64;
      31              : 
      32              : /// The durations of the last scan, parse and analysis of a script in nanoseconds
      33              : #[derive(Clone, Copy, Debug, Default, PartialEq)]
      34              : pub struct ProcessingTimings {
      35              :     /// The duration of the last scan
      36              :     pub scanner_ns: f64,
      37              :     /// The duration of the last parse
      38              :     pub parser_ns: f64,
      39              :     /// The duration of the last analysis
      40              :     pub analyzer_ns: f64,
      41              : }
      42              : 
      43              : /// A script text and its latest scanned, parsed and analyzed versions
      44              : pub struct Script {
      45              :     /// The catalog entry id
      46              :     catalog_entry_id: CatalogEntryId,
      47              :     /// The text
      48              :     text: Rope,
      49              :     /// The text version
      50              :     text_version: TextVersion,
      51              :     /// The latest scanned script
      52              :     pub scanned: Option<Arc<ScannedScript>>,
      53              :     /// The latest parsed script
      54              :     pub parsed: Option<Arc<ParsedScript>>,
      55              :     /// The latest analyzed script
      56              :     pub analyzed: Option<Arc<AnalyzedScript>>,
      57              :     /// The results of the scripts of the session that the table parameters read, the next analysis uses them
      58              :     pub table_parameters: Option<Arc<TableParameters>>,
      59              :     /// The text offset of the cursor, reset by every analysis
      60              :     cursor_offset: Option<usize>,
      61              :     /// The durations of the last scan, parse and analysis
      62              :     timings: ProcessingTimings,
      63              : }
      64              : 
      65              : /// A stopwatch for the processing timings. `std::time::Instant` panics on wasm32-unknown-unknown,
      66              : /// where the timings stay 0 until the host provides a clock.
      67              : struct Stopwatch {
      68              :     /// The instant the stopwatch was started
      69              :     #[cfg(not(target_arch = "wasm32"))]
      70              :     start: std::time::Instant,
      71              : }
      72              : 
      73              : impl Stopwatch {
      74         7436 :     fn start() -> Self {
      75         7436 :         Self {
      76         7436 :             #[cfg(not(target_arch = "wasm32"))]
      77         7436 :             start: std::time::Instant::now(),
      78         7436 :         }
      79         7436 :     }
      80              : 
      81              :     /// The elapsed nanoseconds
      82         7436 :     fn elapsed_ns(&self) -> f64 {
      83              :         #[cfg(not(target_arch = "wasm32"))]
      84              :         {
      85         7436 :             self.start.elapsed().as_nanos() as f64
      86              :         }
      87              :         #[cfg(target_arch = "wasm32")]
      88              :         {
      89              :             0.0
      90              :         }
      91         7436 :     }
      92              : }
      93              : 
      94              : impl Script {
      95              :     /// Create a script with a new entry id of the catalog
      96         2992 :     pub fn new(catalog: &Catalog) -> Self {
      97         2992 :         Self::with_entry_id(catalog.allocate_entry_id())
      98         2992 :     }
      99              : 
     100         2993 :     pub fn with_entry_id(catalog_entry_id: CatalogEntryId) -> Self {
     101         2993 :         Self {
     102         2993 :             catalog_entry_id,
     103         2993 :             text: Rope::new(),
     104         2993 :             text_version: 0,
     105         2993 :             scanned: None,
     106         2993 :             parsed: None,
     107         2993 :             analyzed: None,
     108         2993 :             table_parameters: None,
     109         2993 :             cursor_offset: None,
     110         2993 :             timings: ProcessingTimings::default(),
     111         2993 :         }
     112         2993 :     }
     113              : 
     114         4188 :     pub fn catalog_entry_id(&self) -> CatalogEntryId {
     115         4188 :         self.catalog_entry_id
     116         4188 :     }
     117              : 
     118         1732 :     pub fn text_version(&self) -> TextVersion {
     119         1732 :         self.text_version
     120         1732 :     }
     121              : 
     122          415 :     pub fn text(&self) -> &Rope {
     123          415 :         &self.text
     124          415 :     }
     125              : 
     126              :     /// Overwrite the text version. Only for tests that need versions beyond 32 bits.
     127              :     #[doc(hidden)]
     128            1 :     pub fn set_text_version(&mut self, text_version: TextVersion) {
     129            1 :         self.text_version = text_version;
     130            1 :     }
     131              : 
     132              :     /// Replace the text with an edited rope and bump the version. Unlike `replace_text`, the texts
     133              :     /// are not compared: the caller knows that the text changed.
     134           83 :     pub fn set_text(&mut self, text: Rope) {
     135           83 :         self.text = text;
     136           83 :         self.text_version += 1;
     137           83 :     }
     138              : 
     139              :     /// Insert text at a code point offset. Offsets past the end append.
     140          541 :     pub fn insert_text_at(&mut self, char_idx: usize, text: &str) {
     141          541 :         if text.is_empty() {
     142            2 :             return;
     143          539 :         }
     144          539 :         let char_idx = char_idx.min(self.text.len_chars());
     145          539 :         self.text.insert(char_idx, text);
     146          539 :         self.text_version += 1;
     147          541 :     }
     148              : 
     149              :     /// Erase a code point range
     150           51 :     pub fn erase_text_range(&mut self, char_idx: usize, count: usize) {
     151           51 :         let len = self.text.len_chars();
     152           51 :         if count == 0 || char_idx >= len {
     153            4 :             return;
     154           47 :         }
     155           47 :         let end = (char_idx + count).min(len);
     156           47 :         self.text.remove(char_idx..end);
     157           47 :         self.text_version += 1;
     158           51 :     }
     159              : 
     160              :     /// Replace the text, a no-op if the text is unchanged
     161         1620 :     pub fn replace_text(&mut self, text: &str) {
     162         1620 :         if text.len() == self.text.len_bytes() && self.text == text {
     163           10 :             return;
     164         1610 :         }
     165         1610 :         self.text = Rope::from_str(text);
     166         1610 :         self.text_version += 1;
     167         1620 :     }
     168              : 
     169              :     /// The text as a string
     170         4033 :     pub fn to_text(&self) -> String {
     171         4033 :         self.text.to_string()
     172         4033 :     }
     173              : 
     174              :     /// The text of a byte span, empty if the span begins past the end. The span
     175              :     /// is clamped to the text; characters cut by the span bounds become replacement characters.
     176            5 :     pub fn to_text_span(&self, span: TextSpan) -> String {
     177            5 :         let len = self.text.len_bytes();
     178            5 :         let begin = span.offset as usize;
     179            5 :         if begin >= len {
     180            1 :             return String::new();
     181            4 :         }
     182            4 :         let end = (begin + span.length as usize).min(len);
     183            4 :         let bytes: Vec<u8> = self.text.bytes_at(begin).take(end - begin).collect();
     184            4 :         String::from_utf8_lossy(&bytes).into_owned()
     185            5 :     }
     186              : 
     187              :     /// Scan the script unconditionally
     188         2184 :     pub fn scan(&mut self) {
     189         2184 :         let watch = Stopwatch::start();
     190         2184 :         let text = self.text.to_string();
     191         2184 :         self.scanned = Some(Arc::new(scan(&text, self.text_version, self.catalog_entry_id)));
     192         2184 :         self.timings.scanner_ns = watch.elapsed_ns();
     193         2184 :     }
     194              : 
     195              :     /// Parse the script unconditionally, scanning first if the scan is missing or outdated
     196         2179 :     pub fn parse(&mut self) {
     197         2179 :         if self.scanned.as_ref().is_none_or(|s| s.text_version != self.text_version) {
     198          483 :             self.scan();
     199         2085 :         }
     200              :         // The parser registers keyword names and the replacements of obfuscated literals in a copy
     201              :         // of the scanned script (in place if it is not shared), which becomes the scanned script
     202         2179 :         let watch = Stopwatch::start();
     203         2179 :         let scanned = self.scanned.take().unwrap();
     204         2179 :         let parsed = parse(scanned);
     205         2179 :         self.scanned = Some(parsed.scanned.clone());
     206         2179 :         self.parsed = Some(Arc::new(parsed));
     207         2179 :         self.timings.parser_ns = watch.elapsed_ns();
     208         2179 :     }
     209              : 
     210              :     /// Scan and parse if the text changed
     211          882 :     fn parse_if_outdated(&mut self) {
     212          882 :         if self.scanned.as_ref().is_none_or(|s| s.text_version != self.text_version) {
     213          403 :             self.scan();
     214          763 :         }
     215          882 :         let current = match (&self.parsed, &self.scanned) {
     216          531 :             (Some(p), Some(s)) => Arc::ptr_eq(&p.scanned, s),
     217          351 :             _ => false,
     218              :         };
     219          882 :         if !current {
     220          403 :             self.parse();
     221          763 :         }
     222          882 :     }
     223              : 
     224              :     /// Parse if the parsed script is missing or was parsed from another text version (the check of
     225              :     /// `statement_text`, `rewrite_vis_board` and `compute_signature`)
     226           13 :     fn parse_if_text_outdated(&mut self) {
     227           13 :         if self.parsed.as_ref().is_none_or(|p| p.scanned.text_version != self.text_version) {
     228           12 :             self.parse();
     229           12 :         }
     230           13 :     }
     231              : 
     232              :     /// Analyze the script
     233         3075 :     pub fn analyze(&mut self, catalog: &Catalog, parse_if_outdated: bool) -> Result<()> {
     234         3075 :         if parse_if_outdated {
     235          876 :             self.parse_if_outdated();
     236         3070 :         }
     237         3075 :         let Some(parsed) = self.parsed.clone() else {
     238            2 :             return Err(Error::status(StatusCode::SCRIPT_NOT_PARSED));
     239              :         };
     240         3073 :         self.cursor_offset = None;
     241         3073 :         let watch = Stopwatch::start();
     242         3073 :         self.analyzed = Some(Arc::new(analyze_with(parsed, catalog, self.table_parameters.as_ref())?));
     243         3073 :         self.timings.analyzer_ns = watch.elapsed_ns();
     244         3073 :         Ok(())
     245         3075 :     }
     246              : 
     247              :     /// The scanned, parsed and analyzed script if the analysis is current with the text, whatever catalog it was
     248              :     /// analyzed with
     249         5450 :     pub fn current_analysis(&self) -> Option<(&Arc<ScannedScript>, &Arc<ParsedScript>, &Arc<AnalyzedScript>)> {
     250         5450 :         let (scanned, parsed, analyzed) = (self.scanned.as_ref()?, self.parsed.as_ref()?, self.analyzed.as_ref()?);
     251         5200 :         let current = Arc::ptr_eq(&analyzed.parsed, parsed) && Arc::ptr_eq(&parsed.scanned, scanned) && scanned.text_version == self.text_version;
     252         5200 :         current.then_some((scanned, parsed, analyzed))
     253         5450 :     }
     254              : 
     255              :     /// Is the analysis current with the script results of the script? An analysis without table parameters reads
     256              :     /// no results, any other was analyzed with the same results.
     257          122 :     pub fn table_parameters_current(&self) -> bool {
     258          122 :         let Some(analyzed) = self.analyzed.as_ref() else { return true };
     259          122 :         if !analyzed.parameters.iter().any(|p| p.kind == crate::analyzer::ParameterKind::Table) {
     260          110 :             return true;
     261           12 :         }
     262           12 :         match (analyzed.table_parameters.as_ref(), self.table_parameters.as_ref()) {
     263            0 :             (None, None) => true,
     264           10 :             (Some(l), Some(r)) => Arc::ptr_eq(l, r),
     265            2 :             _ => false,
     266              :         }
     267          122 :     }
     268              : 
     269              :     /// The parts of the script that a cursor reads
     270          362 :     pub fn view<'a>(&'a self, catalog: &'a Catalog) -> ScriptView<'a> {
     271          362 :         ScriptView {
     272          362 :             scanned: self.scanned.as_ref(),
     273          362 :             parsed: self.parsed.as_ref(),
     274          362 :             analyzed: self.analyzed.as_ref(),
     275          362 :             catalog,
     276          362 :         }
     277          362 :     }
     278              : 
     279              :     /// Move the cursor to a text offset
     280          327 :     pub fn move_cursor<'a>(&'a mut self, catalog: &'a Catalog, text_offset: usize) -> ScriptCursor<'a> {
     281          327 :         self.cursor_offset = Some(text_offset);
     282          327 :         ScriptCursor::place(self.view(catalog), text_offset)
     283          327 :     }
     284              : 
     285              :     /// The text offset of the cursor, if it was moved since the last analysis
     286           44 :     pub fn cursor_offset(&self) -> Option<usize> {
     287           44 :         self.cursor_offset
     288           44 :     }
     289              : 
     290              :     /// Move or reset the cursor without placing it. The cursor is placed when it is read with
     291              :     /// `cursor`.
     292          154 :     pub fn set_cursor_offset(&mut self, text_offset: Option<usize>) {
     293          154 :         self.cursor_offset = text_offset;
     294          154 :     }
     295              : 
     296              :     /// The cursor, if it was moved since the last analysis
     297           35 :     pub fn cursor<'a>(&'a self, catalog: &'a Catalog) -> Option<ScriptCursor<'a>> {
     298           35 :         self.cursor_offset.map(|offset| ScriptCursor::place(self.view(catalog), offset))
     299           35 :     }
     300              : 
     301              :     /// Complete at the cursor
     302          316 :     pub fn complete_at_cursor<'c, 'a>(cursor: &'c ScriptCursor<'a>, limit: usize) -> Result<Completion<'c, 'a>> {
     303          316 :         if cursor.scanner_location.is_none() {
     304            0 :             return Err(Error::status(StatusCode::COMPLETION_MISSES_SCANNER_TOKEN));
     305          316 :         }
     306          316 :         Ok(Completion::compute(cursor, limit))
     307          316 :     }
     308              : 
     309              :     /// Compute the signature
     310            4 :     pub fn compute_signature(&mut self, parse_if_outdated: bool) -> Result<String> {
     311            4 :         if parse_if_outdated {
     312            2 :             self.parse_if_text_outdated();
     313            2 :         }
     314            4 :         let parsed = self.parsed.as_ref().ok_or_else(|| Error::status(StatusCode::SCRIPT_NOT_PARSED))?;
     315            3 :         crate::signature::compute_signature(parsed)
     316            4 :     }
     317              : 
     318              :     /// The text of the first statement without its separator or surrounding trivia
     319            6 :     pub fn statement_text(&mut self, parse_if_outdated: bool) -> Result<String> {
     320            6 :         if parse_if_outdated {
     321            2 :             self.parse_if_text_outdated();
     322            5 :         }
     323            6 :         let parsed = self.parsed.as_ref().ok_or_else(|| Error::status(StatusCode::SCRIPT_NOT_PARSED))?;
     324            5 :         if parsed.statements.is_empty() {
     325            2 :             return Ok(String::new());
     326            3 :         }
     327            3 :         let span = parsed.associate_descriptions()[0].statement_span;
     328            3 :         Ok(self.to_text_span(span))
     329            6 :     }
     330              : 
     331              :     /// The parsed script whose spans apply to the current text, parsed first if requested and outdated
     332           86 :     fn parsed_for_rewrite(&mut self, parse_if_outdated: bool) -> Result<Arc<ParsedScript>> {
     333           86 :         if parse_if_outdated {
     334            9 :             self.parse_if_text_outdated();
     335           77 :         }
     336           86 :         self.parsed.clone().ok_or_else(|| Error::status(StatusCode::SCRIPT_NOT_PARSED))
     337           86 :     }
     338              : 
     339              :     /// The annotation of a statement: the comment tags in front of it (see `annotation`)
     340            6 :     pub fn read_annotation(&mut self, statement_id: usize, parse_if_outdated: bool) -> Result<crate::annotation::Annotation> {
     341            6 :         let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
     342            5 :         crate::annotation::read_annotation(&parsed, statement_id)
     343            6 :     }
     344              : 
     345              :     /// Replace the annotation of a statement, printed as one comment in place of its tagged comments. An
     346              :     /// empty annotation removes them. Returns the new text, the script is not changed.
     347              :     ///
     348              :     /// The spans of the parsed script are applied to the current text, which differs from the parsed
     349              :     /// text if the script was edited and `parse_if_outdated` is false (see docs/design/core_quirks.md).
     350            3 :     pub fn rewrite_annotation(
     351            3 :         &mut self,
     352            3 :         statement_id: usize,
     353            3 :         annotation: &crate::annotation::Annotation,
     354            3 :         parse_if_outdated: bool,
     355            3 :     ) -> Result<String> {
     356            3 :         let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
     357            3 :         crate::annotation::write_annotation(&self.text.to_string(), &parsed, statement_id, annotation)
     358            3 :     }
     359              : 
     360              :     /// Set or remove the renderer of a query, its `@vis vegalite`, `markdown` or `table` tag (see
     361              :     /// `vis_rewrite`). Returns the new text, the script is not changed.
     362              :     ///
     363              :     /// The spans of the parsed script are applied to the current text, which differs from the parsed
     364              :     /// text if the script was edited and `parse_if_outdated` is false (see docs/design/core_quirks.md).
     365           29 :     pub fn rewrite_vis_renderer(
     366           29 :         &mut self,
     367           29 :         statement_id: usize,
     368           29 :         renderer: Option<&VisRenderer>,
     369           29 :         parse_if_outdated: bool,
     370           29 :     ) -> Result<String> {
     371           29 :         let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
     372           28 :         crate::vis_rewrite::rewrite_vis_renderer(&self.text.to_string(), &parsed, statement_id, renderer)
     373           29 :     }
     374              : 
     375              :     /// Set or remove the dashboard placement of a statement, its `@board` tag. Returns the new text like
     376              :     /// `rewrite_vis_renderer`.
     377           30 :     pub fn rewrite_vis_board(&mut self, statement_id: usize, board: Option<&VisBoard>, parse_if_outdated: bool) -> Result<String> {
     378           30 :         let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
     379           30 :         crate::vis_rewrite::rewrite_vis_board(&self.text.to_string(), &parsed, statement_id, board)
     380           30 :     }
     381              : 
     382              :     /// Set or remove the position of the script on the dataflow canvas, its `@flow` tag. Returns the new
     383              :     /// text like `rewrite_vis_renderer`.
     384           11 :     pub fn rewrite_vis_flow(&mut self, flow: Option<&VisFlow>, parse_if_outdated: bool) -> Result<String> {
     385           11 :         let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
     386           11 :         crate::vis_rewrite::rewrite_vis_flow(&self.text.to_string(), &parsed, flow)
     387           11 :     }
     388              : 
     389              :     /// Set or remove the value of an input of the script, its `@param` tag. A new tag goes to the statement that uses
     390              :     /// the input first. Returns the new text like `rewrite_vis_renderer`.
     391            7 :     pub fn rewrite_param_tag(&mut self, name: &str, value: Option<&str>, parse_if_outdated: bool) -> Result<String> {
     392            7 :         let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
     393            6 :         let first_use = self
     394            6 :             .analyzed
     395            6 :             .as_ref()
     396            6 :             .filter(|a| Arc::ptr_eq(&a.parsed, &parsed))
     397            6 :             .and_then(|a| {
     398            2 :                 a.parameters
     399            2 :                     .iter()
     400            2 :                     .find(|p| p.kind == crate::analyzer::ParameterKind::Scalar && p.name.as_deref() == Some(name))
     401            2 :                     .and_then(|p| p.ast_statement_id)
     402            2 :             })
     403            6 :             .map(|s| s as usize);
     404            6 :         crate::param_rewrite::rewrite_param_tag(&self.text.to_string(), &parsed, name, value, first_use)
     405            7 :     }
     406              : 
     407              :     /// Visualize the terminal query with a Vega-Lite spec: replace its renderer tag (`@vis vegalite`,
     408              :     /// `markdown` or `table`) by `@vis vegalite <spec>`, or add one. The annotation of the query is
     409              :     /// consolidated into one comment (see `vis_rewrite`).
     410              :     ///
     411              :     /// The spec must be a JSON object, it is written compactly with its key order kept (`compact_json`).
     412              :     /// `/*` and `*/` in its strings are escaped as `/\u002a` and `*\/`, they would nest or end a block comment.
     413              :     /// The spans of the parsed script are applied to the current text, as in `rewrite_vis_board`.
     414              :     ///
     415              :     /// Returns the rewritten text and the span of the written tag in it, from `@vis` to the end of the spec.
     416           29 :     pub fn rewrite_vegalite(&mut self, spec: &str, parse_if_outdated: bool) -> Result<(String, TextSpan)> {
     417           29 :         if parse_if_outdated {
     418            0 :             self.parse_if_text_outdated();
     419           29 :         }
     420           29 :         let not_parsed = || Error::status(StatusCode::SCRIPT_NOT_PARSED);
     421           29 :         let parsed = self.parsed.clone().ok_or_else(not_parsed)?;
     422           29 :         if !parsed.errors.is_empty() || parsed.statements.is_empty() {
     423            2 :             return Err(not_parsed());
     424           27 :         }
     425           27 :         let statement_id = parsed.statements.len() - 1;
     426           27 :         if parsed.statements[statement_id].statement_type != StatementType::SELECT {
     427            1 :             return Err(Error::invalid_argument("the last statement of the script is no query"));
     428           26 :         }
     429           26 :         let spec = match serde_json::from_str(spec) {
     430           24 :             Ok(serde_json::Value::Object(spec)) => spec,
     431            1 :             Ok(_) => return Err(Error::invalid_argument("the Vega-Lite spec is no JSON object")),
     432            1 :             Err(e) => return Err(Error::invalid_argument(format!("the Vega-Lite spec is no valid JSON: {e}"))),
     433              :         };
     434           24 :         let renderer = VisRenderer::VegaLite(spec);
     435           24 :         let (text, span) = crate::vis_rewrite::rewrite_vis_renderer_tag(&self.text.to_string(), &parsed, statement_id, Some(&renderer))?;
     436           24 :         Ok((text, span.expect("a written tag has a span")))
     437           29 :     }
     438              : 
     439              :     /// Scan and parse if requested and outdated, then return the parsed script to format
     440           13 :     fn parsed_for_formatting(&mut self, parse_if_outdated: bool) -> Result<&Arc<ParsedScript>> {
     441           13 :         if parse_if_outdated {
     442            6 :             self.parse_if_outdated();
     443            9 :         }
     444           13 :         self.parsed.as_ref().ok_or_else(|| Error::status(StatusCode::SCRIPT_NOT_PARSED))
     445           13 :     }
     446              : 
     447              :     /// Format the script
     448            5 :     pub fn format(&mut self, config: &FormattingConfigT, parse_if_outdated: bool) -> Result<String> {
     449            5 :         let parsed = self.parsed_for_formatting(parse_if_outdated)?;
     450            4 :         Ok(Formatter::new(parsed).format(config))
     451            5 :     }
     452              : 
     453              :     /// Format the script and return the raw bytes, which may contain partial UTF-8 sequences where
     454              :     /// comments were wrapped (see `Formatter::format_bytes`)
     455            2 :     pub fn format_bytes(&mut self, config: &FormattingConfigT, parse_if_outdated: bool) -> Result<Vec<u8>> {
     456            2 :         let parsed = self.parsed_for_formatting(parse_if_outdated)?;
     457            2 :         Ok(Formatter::new(parsed).format_bytes(config))
     458            2 :     }
     459              : 
     460              :     /// The nodes that formatting could not represent
     461            2 :     pub fn unformattable_nodes(&mut self, config: &FormattingConfigT, parse_if_outdated: bool) -> Result<Vec<u32>> {
     462            2 :         let parsed = self.parsed_for_formatting(parse_if_outdated)?;
     463            2 :         let mut formatter = Formatter::new(parsed);
     464            2 :         formatter.format_bytes(config);
     465            2 :         Ok(formatter.unformattable_nodes())
     466            2 :     }
     467              : 
     468              :     /// Can formatting represent every node and does the script parse without errors?
     469              :     ///
     470              :     /// Statements with syntax errors are copied as written, which is not formatting them.
     471            4 :     pub fn is_fully_formattable(&mut self, config: &FormattingConfigT, parse_if_outdated: bool) -> Result<bool> {
     472            4 :         let parsed = self.parsed_for_formatting(parse_if_outdated)?;
     473            4 :         let mut formatter = Formatter::new(parsed);
     474            4 :         formatter.format_bytes(config);
     475            4 :         Ok(formatter.is_fully_formatted())
     476            4 :     }
     477              : 
     478              :     /// The durations of the last scan, parse and analysis in nanoseconds
     479           99 :     pub fn timings(&self) -> ProcessingTimings {
     480           99 :         self.timings
     481           99 :     }
     482              : 
     483              :     /// The memory statistics.
     484              :     ///
     485              :     /// They describe the latest analyzed script and the parsed and scanned script it was analyzed
     486              :     /// from, and are 0 except for the rope size before the first analysis. The rope and input sizes
     487              :     /// are text lengths, the AST size is the node count times the node size, and the name index size
     488              :     /// is its entry count. The others measure the Rust data structures:
     489              :     /// - `scanner_symbol_bytes` is the size of the symbol vector.
     490              :     /// - `scanner_name_dictionary_bytes` counts the registered names and their texts.
     491              :     /// - `analyzer_description_bytes` multiplies the element counts of the declarations, references,
     492              :     ///   expressions and scopes with the sizes of their types.
     493              :     /// - `analyzer_name_index_bytes` is the size of the suffix vector.
     494          101 :     pub fn memory_statistics(&self) -> ScriptMemoryStatisticsT {
     495              :         use std::mem::size_of;
     496          101 :         let mut memory = ScriptMemoryStatisticsT::default();
     497          101 :         memory.rope_bytes = self.text.len_bytes() as u32;
     498          101 :         let Some(analyzed) = self.analyzed.as_ref() else {
     499            2 :             return memory;
     500              :         };
     501           99 :         let entry = &analyzed.entry;
     502           99 :         let table_column_bytes: usize = entry
     503           99 :             .table_declarations
     504           99 :             .iter()
     505           99 :             .map(|t| t.table_columns.len() * size_of::<crate::catalog::TableColumn>())
     506           99 :             .sum();
     507           99 :         let description_bytes = entry.database_references.len() * size_of::<crate::catalog::DatabaseReference>()
     508           99 :             + entry.schema_references.len() * size_of::<crate::catalog::SchemaReference>()
     509           99 :             + entry.table_declarations.len() * size_of::<crate::catalog::TableDeclaration>()
     510           99 :             + table_column_bytes
     511           99 :             + analyzed.table_references.len() * size_of::<crate::analyzer::TableReference>()
     512           99 :             + analyzed.expressions.len() * size_of::<crate::analyzer::Expression>()
     513           99 :             + analyzed.function_arguments.len() * size_of::<crate::analyzer::FunctionArgument>()
     514           99 :             + analyzed.name_scopes.len() * size_of::<crate::analyzer::NameScope>();
     515           99 :         let latest = &mut memory.latest_script;
     516           99 :         latest.analyzer_description_bytes = description_bytes as u32;
     517           99 :         let index = analyzed.name_search_index();
     518           99 :         if index.is_built() {
     519            1 :             latest.analyzer_name_index_size = index.len() as u32;
     520            1 :             latest.analyzer_name_index_bytes = index.byte_size() as u32;
     521           98 :         }
     522           99 :         let parsed = &analyzed.parsed;
     523           99 :         latest.parser_ast_bytes = (parsed.nodes.len() * size_of::<Node>()) as u32;
     524           99 :         let scanned = &parsed.scanned;
     525           99 :         latest.scanner_input_bytes = scanned.text.len() as u32;
     526           99 :         latest.scanner_symbol_bytes = (scanned.symbols.len() * size_of::<dashql_parser::scanner::Symbol>()) as u32;
     527           99 :         let name_bytes: usize = scanned
     528           99 :             .names
     529           99 :             .iter()
     530          171 :             .map(|n| n.text.len() + size_of::<dashql_parser::names::RegisteredName>())
     531           99 :             .sum();
     532           99 :         latest.scanner_name_dictionary_bytes = name_bytes as u32;
     533           99 :         memory
     534          101 :     }
     535              : 
     536              :     /// The processing statistics
     537            2 :     pub fn statistics(&self) -> ScriptStatisticsT {
     538            2 :         let mut statistics = ScriptStatisticsT::default();
     539            2 :         let mut timings = ScriptProcessingTimingsT::default();
     540            2 :         timings.scanner_last_elapsed = self.timings.scanner_ns;
     541            2 :         timings.parser_last_elapsed = self.timings.parser_ns;
     542            2 :         timings.analyzer_last_elapsed = self.timings.analyzer_ns;
     543            2 :         statistics.timings = Some(timings);
     544            2 :         statistics.memory = Some(self.memory_statistics());
     545            2 :         statistics
     546            2 :     }
     547              : }
        

Generated by: LCOV version 2.0-1