LCOV - code coverage report
Current view: top level - dashql-parser/src/parser - mod.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 81.3 % 182 148
Test Date: 2026-11-06 17:46:50

            Line data    Source code
       1              : //! The parser: the grammar actions (`grammar/rules_rs`), the parse context, and the LALR(1) driver.
       2              : 
       3              : pub mod actions;
       4              : pub mod context;
       5              : pub mod driver;
       6              : pub mod probe;
       7              : pub mod enums;
       8              : pub mod value;
       9              : 
      10              : use crate::comment_blocks::{group_comments, CommentBlocks};
      11              : use crate::scanner::ScannedScript;
      12              : use crate::span::TextSpan;
      13              : pub use context::{ParseError, Statement};
      14              : use dashql_buffers::parser::{AttributeKey, Node, NodeType};
      15              : use std::sync::Arc;
      16              : 
      17              : /// A parsed script
      18              : #[derive(Clone, Debug)]
      19              : pub struct ParsedScript {
      20              :     /// The scanned script, including the names that the parser registered
      21              :     pub scanned: Arc<ScannedScript>,
      22              :     /// The AST nodes in post-order
      23              :     pub nodes: Vec<Node>,
      24              :     /// The statements
      25              :     pub statements: Vec<Statement>,
      26              :     /// The parser errors
      27              :     pub errors: Vec<ParseError>,
      28              :     /// The comments grouped into blocks
      29              :     pub comment_blocks: CommentBlocks,
      30              : }
      31              : 
      32              : /// Source spans and description comments of a statement
      33              : #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
      34              : pub struct StatementDescription {
      35              :     /// The text of the statement root
      36              :     pub statement_span: TextSpan,
      37              :     /// The statement source, including the terminating semicolon and trailing comments on its line
      38              :     pub source_span: TextSpan,
      39              :     /// The description comments (a slice of the comments of the script)
      40              :     pub description_begin: u32,
      41              :     /// The number of description comments
      42              :     pub description_count: u32,
      43              :     /// The comment blocks of the description comments (a slice of the comment blocks of the script)
      44              :     pub description_blocks_begin: u32,
      45              :     /// The number of comment blocks of the description comments
      46              :     pub description_blocks_count: u32,
      47              : }
      48              : 
      49              : /// Only whitespace and semicolons
      50         3590 : fn is_description_gap(text: &[u8], begin: usize, end: usize) -> bool {
      51         3590 :     text[begin..end.max(begin)].iter().all(|c| matches!(c, b' ' | b'\t' | b'\n' | b'\r' | b'\x0c' | b';'))
      52         3590 : }
      53              : 
      54              : /// The end of the statement source, including comments that trail its terminating semicolon on the
      55              : /// same line
      56        21387 : fn statement_source_end(text: &[u8], sql_end: usize, limit: usize, comments: &[TextSpan]) -> usize {
      57        21387 :     let mut end = sql_end;
      58        21389 :     for comment in comments {
      59         2812 :         let offset = comment.offset as usize;
      60         2812 :         if offset < end || offset >= limit {
      61         2560 :             continue;
      62          252 :         }
      63          252 :         if !is_description_gap(text, end, offset) {
      64            1 :             break;
      65          251 :         }
      66          251 :         if text[end..offset].iter().any(|&c| c == b'\n' || c == b'\r') {
      67          206 :             break;
      68           45 :         }
      69           45 :         end = comment.end() as usize;
      70              :     }
      71        21387 :     end
      72        21387 : }
      73              : 
      74              : /// A leading description comment must start on a fresh line when it follows prior SQL
      75         1428 : fn is_leading_description_comment_gap(text: &[u8], previous_end: usize, comment: TextSpan) -> bool {
      76         1428 :     let offset = comment.offset as usize;
      77         1428 :     if !is_description_gap(text, previous_end, offset) {
      78            3 :         return false;
      79         1425 :     }
      80         1425 :     if previous_end == 0 || offset == 0 {
      81         1218 :         return true;
      82          207 :     }
      83          207 :     text[previous_end..offset].iter().any(|&c| c == b'\n' || c == b'\r')
      84         1428 : }
      85              : 
      86              : impl ParsedScript {
      87              :     /// The children of a node
      88         2737 :     pub fn children(&self, node: &Node) -> &[Node] {
      89         2737 :         let begin = node.children_begin_or_value() as usize;
      90         2737 :         self.nodes.get(begin..begin + node.children_count() as usize).unwrap_or_default()
      91         2737 :     }
      92              : 
      93              :     /// The text of a node
      94          621 :     pub fn node_text(&self, node: &Node) -> &str {
      95          621 :         let scanned = &*self.scanned;
      96          621 :         let span = context::resolve_text_span(&scanned.text, &scanned.symbols, *node.symbol_span());
      97          621 :         let begin = (span.offset as usize).min(scanned.text.len());
      98          621 :         let end = (begin + span.length as usize).min(scanned.text.len());
      99          621 :         &scanned.text[begin..end]
     100          621 :     }
     101              : 
     102              :     /// Derive source spans and contiguous description comment blocks
     103        16591 :     pub fn associate_descriptions(&self) -> Vec<StatementDescription> {
     104        16591 :         let scanned = &*self.scanned;
     105        16591 :         let input = scanned.text.as_bytes();
     106        16591 :         let comments = &scanned.comments;
     107        27081 :         let resolve = |node: u32| context::resolve_text_span(&scanned.text, &scanned.symbols, *self.nodes[node as usize].symbol_span());
     108        16591 :         let mut metadata = Vec::with_capacity(self.statements.len());
     109              : 
     110              :         // AST root spans exclude statement separators and comments. Extend each root to its first
     111              :         // following semicolon before the next statement's root.
     112        21663 :         for (i, statement) in self.statements.iter().enumerate() {
     113        21387 :             let root_span = resolve(statement.root);
     114        21387 :             let mut end = root_span.end() as usize;
     115        21387 :             let next_begin = match self.statements.get(i + 1) {
     116         5469 :                 Some(next) => resolve(next.root).offset as usize,
     117        15918 :                 None => input.len(),
     118              :             };
     119        21387 :             let mut cursor = end;
     120        21754 :             while cursor < next_begin {
     121        16233 :                 if let Some(comment) = comments.iter().find(|c| (c.offset as usize) <= cursor && cursor < c.end() as usize) {
     122           20 :                     cursor = comment.end() as usize;
     123           20 :                     continue;
     124        16208 :                 }
     125        16208 :                 if input[cursor] == b';' {
     126        15861 :                     end = cursor + 1;
     127        15861 :                     break;
     128          347 :                 }
     129          347 :                 cursor += 1;
     130              :             }
     131        21387 :             let end = statement_source_end(input, end, next_begin, comments);
     132        21387 :             metadata.push(StatementDescription {
     133        21387 :                 statement_span: root_span,
     134        21387 :                 source_span: TextSpan::new(root_span.offset, (end - root_span.offset as usize) as u32),
     135        21387 :                 ..Default::default()
     136        21387 :             });
     137              :         }
     138              : 
     139        16591 :         let mut comment_cursor = 0;
     140        16591 :         let mut previous_end = 0;
     141        16591 :         let mut block_cursor = 0;
     142        16591 :         let blocks = &self.comment_blocks.blocks;
     143        21663 :         for statement in metadata.iter_mut() {
     144        21407 :             while comment_cursor < comments.len() && (comments[comment_cursor].offset as usize) < previous_end {
     145           20 :                 comment_cursor += 1;
     146           20 :             }
     147        21387 :             let mut begin = comment_cursor;
     148        21387 :             let mut end = begin;
     149        21387 :             let mut cursor = previous_end;
     150        23303 :             while end < comments.len() && comments[end].offset < statement.source_span.offset {
     151         1916 :                 let comment = comments[end];
     152         1916 :                 let allowed = if end == begin {
     153         1428 :                     is_leading_description_comment_gap(input, cursor, comment)
     154              :                 } else {
     155          488 :                     is_description_gap(input, cursor, comment.offset as usize)
     156              :                 };
     157         1916 :                 if !allowed {
     158            6 :                     begin = end + 1;
     159         1910 :                 }
     160         1916 :                 cursor = comment.end() as usize;
     161         1916 :                 end += 1;
     162              :             }
     163        21387 :             if begin < end && is_description_gap(input, cursor, statement.source_span.offset as usize) {
     164         1422 :                 statement.description_begin = begin as u32;
     165         1422 :                 statement.description_count = (end - begin) as u32;
     166              :                 // Blocks never straddle the description comments
     167         1447 :                 while block_cursor < blocks.len() && (blocks[block_cursor].comments_begin as usize) < begin {
     168           25 :                     block_cursor += 1;
     169           25 :                 }
     170         1422 :                 statement.description_blocks_begin = block_cursor as u32;
     171         3421 :                 while block_cursor < blocks.len() && (blocks[block_cursor].comments_begin as usize) < end {
     172         1999 :                     block_cursor += 1;
     173         1999 :                 }
     174         1422 :                 statement.description_blocks_count = block_cursor as u32 - statement.description_blocks_begin;
     175        19965 :             }
     176        21387 :             previous_end = statement.source_span.end() as usize;
     177        21387 :             comment_cursor = end;
     178              :         }
     179        16591 :         metadata
     180        16591 :     }
     181              : }
     182              : 
     183              : /// Parse a scanned script.
     184              : ///
     185              : /// The parser registers names (keywords used as names) and may update the replacements of
     186              : /// obfuscated literals, so the returned parsed script owns an updated copy of the scanned script
     187              : /// unless `scanned` is not shared.
     188         7386 : pub fn parse(scanned: Arc<ScannedScript>) -> ParsedScript {
     189         7386 :     let mut scanned = Arc::try_unwrap(scanned).unwrap_or_else(|shared| (*shared).clone());
     190         7386 :     let names = std::mem::take(&mut scanned.names);
     191         7386 :     let state = {
     192         7386 :         let mut ctx = context::ParseContext::new(&scanned.text, &scanned.symbols, names);
     193         7386 :         driver::parse(&mut ctx);
     194         7386 :         ctx.into_state()
     195              :     };
     196         7386 :     scanned.names = state.names;
     197              : 
     198         7386 :     let comment_blocks = group_comments(&scanned.text, &scanned.comments);
     199         7386 :     replace_obfuscated_cast_literals(&mut scanned, &state.nodes);
     200         7386 :     ParsedScript {
     201         7386 :         scanned: Arc::new(scanned),
     202         7386 :         nodes: state.nodes,
     203         7386 :         statements: state.statements,
     204         7386 :         errors: state.errors,
     205         7386 :         comment_blocks,
     206         7386 :     }
     207         7386 : }
     208              : 
     209              : /// Obfuscated strings that are cast to a type need a replacement that the type accepts, e.g.
     210              : /// `timestamptz /*String(A834)*/` cannot become `timestamptz 'A834'`.
     211         7386 : fn replace_obfuscated_cast_literals(scanned: &mut ScannedScript, nodes: &[Node]) {
     212         7386 :     if scanned.obfuscated_literals.is_empty() {
     213         7376 :         return;
     214           10 :     }
     215          933 :     for node in nodes {
     216          933 :         if node.node_type() != NodeType::LITERAL_STRING || node.attribute_key() != AttributeKey::SQL_CONST_CAST_VALUE {
     217          933 :             continue;
     218            0 :         }
     219            0 :         let cast = nodes[node.parent() as usize];
     220            0 :         if !matches!(
     221            0 :             cast.node_type(),
     222              :             NodeType::OBJECT_SQL_CONST_TYPE_CAST | NodeType::OBJECT_SQL_CONST_FUNCTION_CAST | NodeType::OBJECT_SQL_CONST_INTERVAL_CAST
     223              :         ) {
     224            0 :             continue;
     225            0 :         }
     226            0 :         let literal_span = context::resolve_text_span(&scanned.text, &scanned.symbols, *node.symbol_span());
     227            0 :         let Ok(i) = scanned
     228            0 :             .obfuscated_literals
     229            0 :             .binary_search_by_key(&literal_span.offset, |l| l.location.offset)
     230              :         else {
     231            0 :             continue;
     232              :         };
     233              :         // The type is the text of the cast in front of the literal, e.g. "timestamp with time zone"
     234            0 :         let cast_span = context::resolve_text_span(&scanned.text, &scanned.symbols, *cast.symbol_span());
     235            0 :         let type_text = scanned.text[cast_span.offset as usize..literal_span.offset as usize].to_ascii_lowercase();
     236            0 :         let literal = &mut scanned.obfuscated_literals[i];
     237            0 :         let hash = literal.replacement[1..5].to_string();
     238            0 :         let has = |s: &str| type_text.contains(s);
     239            0 :         let replacement = if has("timestamp") {
     240            0 :             "'2000-01-01 00:00:00'".to_string()
     241            0 :         } else if has("date") {
     242            0 :             "'2000-01-01'".to_string()
     243            0 :         } else if has("time") {
     244            0 :             "'00:00:00'".to_string()
     245            0 :         } else if has("interval") {
     246            0 :             "'1'".to_string()
     247            0 :         } else if has("uuid") {
     248            0 :             format!("'00000000-0000-0000-0000-00000000{hash}'")
     249            0 :         } else if has("json") {
     250            0 :             "'{}'".to_string()
     251            0 :         } else if has("bool") {
     252            0 :             "'true'".to_string()
     253            0 :         } else if ["int", "numeric", "decimal", "real", "double", "float"].iter().any(|s| has(s)) {
     254            0 :             format!("'{}'", u32::from_str_radix(&hash, 16).unwrap_or(0))
     255              :         } else {
     256            0 :             continue;
     257              :         };
     258            0 :         literal.replacement = replacement;
     259              :     }
     260         7386 : }
     261              : 
     262              : #[cfg(test)]
     263              : mod tests {
     264              :     use super::*;
     265              :     use crate::scanner::scan;
     266              : 
     267            2 :     fn parse_text(text: &str) -> ParsedScript {
     268            2 :         parse(Arc::new(scan(text, 0, 0)))
     269            2 :     }
     270              : 
     271              :     #[test]
     272            1 :     fn parses_select() {
     273            1 :         let parsed = parse_text("select 1");
     274            1 :         assert!(parsed.errors.is_empty(), "{:?}", parsed.errors);
     275            1 :         assert_eq!(parsed.statements.len(), 1);
     276            1 :         let root = parsed.nodes[parsed.statements[0].root as usize];
     277            1 :         assert_eq!(root.node_type(), NodeType::OBJECT_SQL_SELECT);
     278            1 :     }
     279              : 
     280              :     #[test]
     281            1 :     fn reports_syntax_errors() {
     282            1 :         let parsed = parse_text("select * from foo select");
     283            1 :         assert_eq!(parsed.statements.len(), 0);
     284            1 :         assert_eq!(parsed.errors.len(), 1);
     285            1 :         assert!(parsed.errors[0].message.starts_with("syntax error, unexpected SELECT"), "{:?}", parsed.errors);
     286            1 :     }
     287              : }
        

Generated by: LCOV version 2.0-1