1use crate::error::{CdcError, Result};
15use std::path::Path;
16use tokio::fs::File;
17use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21enum ParseState {
22 Normal,
24 SingleQuote,
26 DoubleQuote,
28 Backtick,
30 Bracket,
32}
33
34pub struct SqlStreamParser {
39 state: ParseState,
41 statement_buffer: Vec<u8>,
43 statement_count: usize,
45 count_nonws: usize,
48}
49
50impl SqlStreamParser {
51 pub fn new() -> Self {
53 Self {
54 state: ParseState::Normal,
55 statement_buffer: Vec::with_capacity(512),
56 statement_count: 0,
57 count_nonws: 0,
58 }
59 }
60
61 pub async fn parse_file_from_index_collect(
66 &mut self,
67 file_path: &Path,
68 start_index: usize,
69 ) -> Result<Vec<String>> {
70 let file = File::open(file_path)
71 .await
72 .map_err(|e| CdcError::generic(format!("Failed to open file {file_path:?}: {e}")))?;
73
74 let reader = BufReader::with_capacity(65536, file);
75
76 self.parse_stream_collect(reader, start_index).await
77 }
78
79 pub async fn parse_stream_collect<R>(
84 &mut self,
85 reader: R,
86 start_index: usize,
87 ) -> Result<Vec<String>>
88 where
89 R: AsyncRead + Unpin,
90 {
91 let mut statements: Vec<String> = Vec::new();
92 let buf_reader = BufReader::new(reader);
93 let mut lines = buf_reader.lines();
94
95 self.statement_count = 0;
96 self.statement_buffer.clear();
97 self.state = ParseState::Normal;
98
99 let mut line_statements: Vec<String> = Vec::new();
100 while let Some(line) = lines
101 .next_line()
102 .await
103 .map_err(|e| CdcError::generic(format!("Failed to read line: {e}")))?
104 {
105 line_statements.clear();
106 self.parse_line(&line, &mut line_statements)?;
107 for stmt in line_statements.drain(..) {
108 if self.statement_count >= start_index {
109 statements.push(stmt);
110 }
111 self.statement_count += 1;
112 }
113 }
114
115 if let Some(stmt) = self.finish_statement() {
116 if self.statement_count >= start_index {
117 statements.push(stmt);
118 }
119 self.statement_count += 1;
120 }
121
122 Ok(statements)
123 }
124
125 pub fn parse_line(&mut self, line: &str, out: &mut Vec<String>) -> Result<()> {
127 let bytes = line.as_bytes();
128 let mut i = 0;
129
130 while i < bytes.len() {
131 let byte = bytes[i];
132
133 match self.state {
134 ParseState::Normal => match byte {
135 b'\'' => {
136 self.statement_buffer.push(byte);
137 self.state = ParseState::SingleQuote;
138 }
139 b'"' => {
140 self.statement_buffer.push(byte);
141 self.state = ParseState::DoubleQuote;
142 }
143 b'`' => {
144 self.statement_buffer.push(byte);
145 self.state = ParseState::Backtick;
146 }
147 b'[' => {
148 self.statement_buffer.push(byte);
149 self.state = ParseState::Bracket;
150 }
151 b';' => {
152 if let Some(stmt) = self.take_trimmed_statement() {
153 out.push(stmt);
154 }
155 self.statement_buffer.clear();
156 }
157 _ => {
158 self.statement_buffer.push(byte);
159 }
160 },
161 ParseState::SingleQuote => {
162 self.statement_buffer.push(byte);
163 if byte == b'\'' {
164 if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
165 i += 1;
166 self.statement_buffer.push(bytes[i]);
167 } else {
168 self.state = ParseState::Normal;
169 }
170 }
171 }
172 ParseState::DoubleQuote => {
173 self.statement_buffer.push(byte);
174 if byte == b'"' {
175 if i + 1 < bytes.len() && bytes[i + 1] == b'"' {
176 i += 1;
177 self.statement_buffer.push(bytes[i]);
178 } else {
179 self.state = ParseState::Normal;
180 }
181 }
182 }
183 ParseState::Backtick => {
184 self.statement_buffer.push(byte);
185 if byte == b'`' {
186 if i + 1 < bytes.len() && bytes[i + 1] == b'`' {
187 i += 1;
188 self.statement_buffer.push(bytes[i]);
189 } else {
190 self.state = ParseState::Normal;
191 }
192 }
193 }
194 ParseState::Bracket => {
195 self.statement_buffer.push(byte);
196 if byte == b']' {
197 self.state = ParseState::Normal;
198 }
199 }
200 }
201
202 i += 1;
203 }
204
205 self.statement_buffer.push(b'\n');
206
207 Ok(())
208 }
209
210 pub fn count_line(&mut self, line: &str) -> usize {
217 let mut completed = 0usize;
218 let mut chars = line.chars().peekable();
225 while let Some(ch) = chars.next() {
226 match self.state {
227 ParseState::Normal => match ch {
228 '\'' => {
229 self.state = ParseState::SingleQuote;
230 self.count_nonws += 1;
231 }
232 '"' => {
233 self.state = ParseState::DoubleQuote;
234 self.count_nonws += 1;
235 }
236 '`' => {
237 self.state = ParseState::Backtick;
238 self.count_nonws += 1;
239 }
240 '[' => {
241 self.state = ParseState::Bracket;
242 self.count_nonws += 1;
243 }
244 ';' => {
245 if self.count_nonws > 0 {
246 completed += 1;
247 }
248 self.count_nonws = 0;
249 }
250 _ => {
251 if !ch.is_whitespace() {
252 self.count_nonws += 1;
253 }
254 }
255 },
256 ParseState::SingleQuote => {
257 self.count_nonws += 1;
258 if ch == '\'' {
259 if chars.peek() == Some(&'\'') {
260 chars.next();
261 self.count_nonws += 1;
262 } else {
263 self.state = ParseState::Normal;
264 }
265 }
266 }
267 ParseState::DoubleQuote => {
268 self.count_nonws += 1;
269 if ch == '"' {
270 if chars.peek() == Some(&'"') {
271 chars.next();
272 self.count_nonws += 1;
273 } else {
274 self.state = ParseState::Normal;
275 }
276 }
277 }
278 ParseState::Backtick => {
279 self.count_nonws += 1;
280 if ch == '`' {
281 if chars.peek() == Some(&'`') {
282 chars.next();
283 self.count_nonws += 1;
284 } else {
285 self.state = ParseState::Normal;
286 }
287 }
288 }
289 ParseState::Bracket => {
290 self.count_nonws += 1;
291 if ch == ']' {
292 self.state = ParseState::Normal;
293 }
294 }
295 }
296 }
297 completed
300 }
301
302 pub fn finish_count(&mut self) -> bool {
304 let has = self.count_nonws > 0;
305 self.count_nonws = 0;
306 has
307 }
308
309 pub fn finish_statement(&mut self) -> Option<String> {
311 if self.statement_buffer.is_empty() {
312 return None;
313 }
314
315 let stmt = self.take_trimmed_statement();
316 self.statement_buffer.clear();
317 stmt
318 }
319
320 fn take_trimmed_statement(&mut self) -> Option<String> {
330 let buf = std::mem::take(&mut self.statement_buffer);
331 let mut s = match String::from_utf8(buf) {
332 Ok(s) => s,
333 Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
334 };
335
336 let trimmed_end = s.trim_end().len();
337 s.truncate(trimmed_end);
338 let leading = s.len() - s.trim_start().len();
339 if leading > 0 {
340 s.drain(..leading);
341 }
342
343 if s.is_empty() {
344 None
345 } else {
346 Some(s)
347 }
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use std::path::PathBuf;
355 use tokio::io::AsyncWriteExt;
356
357 async fn create_test_file(content: &str) -> (String, PathBuf) {
358 let temp_dir = std::env::temp_dir().join(format!("pg2any_test_{}", std::process::id()));
359 tokio::fs::create_dir_all(&temp_dir).await.unwrap();
360 let file_path = temp_dir.join(format!(
361 "test_{}.sql",
362 std::time::SystemTime::now()
363 .duration_since(std::time::UNIX_EPOCH)
364 .unwrap()
365 .as_nanos()
366 ));
367 let mut file = tokio::fs::File::create(&file_path).await.unwrap();
368 file.write_all(content.as_bytes()).await.unwrap();
369 file.flush().await.unwrap();
370 (file_path.to_string_lossy().to_string(), temp_dir)
371 }
372
373 #[test]
374 fn test_count_line_matches_parse_line_statement_count() {
375 fn ref_count(case: &str) -> usize {
379 let mut p_ref = SqlStreamParser::new();
380 let mut n = 0usize;
381 for physical in case.split('\n') {
382 let mut o = Vec::new();
383 p_ref.parse_line(physical, &mut o).unwrap();
384 n += o.len();
385 }
386 if p_ref.finish_statement().is_some() {
387 n += 1;
388 }
389 n
390 }
391
392 fn new_count(case: &str) -> usize {
393 let mut p2 = SqlStreamParser::new();
394 let mut n = 0usize;
395 for physical in case.split('\n') {
396 n += p2.count_line(physical);
397 }
398 if p2.finish_count() {
399 n += 1;
400 }
401 n
402 }
403
404 let cases = [
407 "INSERT INTO t (a) VALUES (1);",
408 "INSERT INTO t (a) VALUES ('a;b');", "TRUNCATE TABLE a;\nTRUNCATE TABLE b;\nTRUNCATE TABLE c;", "UPDATE t SET a = '' WHERE id = 2;",
411 "", "\u{a0};", "\u{3000}", "\u{a0}x\u{a0};", " \u{a0} ;", ];
419 for case in cases {
420 assert_eq!(
421 new_count(case),
422 ref_count(case),
423 "count mismatch for case: {case:?}"
424 );
425 }
426
427 let alphabet = ['a', ';', '\'', '"', '`', '[', ']', ' ', '\t', '\u{a0}'];
432 let n = alphabet.len();
433 for len in 0..=4usize {
434 let total = n.pow(len as u32);
435 for mut idx in 0..total {
436 let mut s = String::new();
437 for _ in 0..len {
438 s.push(alphabet[idx % n]);
439 idx /= n;
440 }
441 assert_eq!(
442 new_count(&s),
443 ref_count(&s),
444 "fuzz count mismatch for input: {s:?}"
445 );
446 }
447 }
448 }
449
450 #[tokio::test]
451 async fn test_simple_statements() {
452 let content =
453 "INSERT INTO users VALUES (1, 'Alice');\nINSERT INTO users VALUES (2, 'Bob');\n";
454 let (file_path, _temp_dir) = create_test_file(content).await;
455
456 let mut parser = SqlStreamParser::new();
457
458 let statements = parser
459 .parse_file_from_index_collect(Path::new(&file_path), 0)
460 .await
461 .unwrap();
462
463 assert_eq!(statements.len(), 2);
464 assert_eq!(statements[0], "INSERT INTO users VALUES (1, 'Alice')");
465 assert_eq!(statements[1], "INSERT INTO users VALUES (2, 'Bob')");
466 }
467
468 #[tokio::test]
469 async fn test_escaped_quotes() {
470 let content = "INSERT INTO users VALUES (1, 'O''Neil');\n";
471 let (file_path, _temp_dir) = create_test_file(content).await;
472
473 let mut parser = SqlStreamParser::new();
474
475 let statements = parser
476 .parse_file_from_index_collect(Path::new(&file_path), 0)
477 .await
478 .unwrap();
479
480 assert_eq!(statements.len(), 1);
481 assert_eq!(statements[0], "INSERT INTO users VALUES (1, 'O''Neil')");
482 }
483
484 #[tokio::test]
485 async fn test_multi_line_statements() {
486 let content = "INSERT INTO users\nVALUES (\n 1,\n 'Alice'\n);\n";
487 let (file_path, _temp_dir) = create_test_file(content).await;
488
489 let mut parser = SqlStreamParser::new();
490
491 let statements = parser
492 .parse_file_from_index_collect(Path::new(&file_path), 0)
493 .await
494 .unwrap();
495
496 assert_eq!(statements.len(), 1);
497 assert!(statements[0].contains("INSERT INTO users"));
498 assert!(statements[0].contains("Alice"));
499 }
500
501 #[tokio::test]
502 async fn test_start_index() {
503 let content = "INSERT INTO users VALUES (1, 'Alice');\nINSERT INTO users VALUES (2, 'Bob');\nINSERT INTO users VALUES (3, 'Charlie');\n";
504 let (file_path, _temp_dir) = create_test_file(content).await;
505
506 let mut parser = SqlStreamParser::new();
507
508 let statements = parser
509 .parse_file_from_index_collect(
510 Path::new(&file_path),
511 1, )
513 .await
514 .unwrap();
515
516 assert_eq!(statements.len(), 2); assert_eq!(statements[0], "INSERT INTO users VALUES (2, 'Bob')");
518 assert_eq!(statements[1], "INSERT INTO users VALUES (3, 'Charlie')");
519 }
520
521 #[tokio::test]
522 async fn test_cancellation() {
523 let content = "INSERT INTO users VALUES (1, 'Alice');\nINSERT INTO users VALUES (2, 'Bob');\nINSERT INTO users VALUES (3, 'Charlie');\n";
526 let (file_path, _temp_dir) = create_test_file(content).await;
527
528 let mut parser = SqlStreamParser::new();
529
530 let statements = parser
531 .parse_file_from_index_collect(Path::new(&file_path), 0)
532 .await
533 .unwrap();
534
535 assert_eq!(statements.len(), 3);
537 }
538}