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 : }
|