1use std::sync::LazyLock;
6
7use regex_lite::Regex;
8use serde::Serialize;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
12pub enum DdlOperation {
13 CreateTable {
15 table: String,
17 if_not_exists: bool,
19 },
20 DropTable {
22 table: String,
24 },
25 AlterTableAddColumn {
27 table: String,
29 column: String,
31 data_type: String,
33 has_default: bool,
35 is_not_null: bool,
37 },
38 AlterTableDropColumn {
40 table: String,
42 column: String,
44 },
45 AlterTableAlterColumn {
47 table: String,
49 column: String,
51 },
52 CreateIndex {
54 name: String,
56 table: String,
58 is_concurrent: bool,
60 is_unique: bool,
62 },
63 DropIndex {
65 name: String,
67 },
68 CreateView {
70 name: String,
72 is_materialized: bool,
74 },
75 DropView {
77 name: String,
79 },
80 CreateFunction {
82 name: String,
84 },
85 DropFunction {
87 name: String,
89 },
90 AddConstraint {
92 table: String,
94 constraint_type: String,
96 },
97 DropConstraint {
99 table: String,
101 name: String,
103 },
104 CreateEnum {
106 name: String,
108 },
109 TruncateTable {
111 table: String,
113 },
114 Other {
116 statement_preview: String,
118 },
119}
120
121impl std::fmt::Display for DdlOperation {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 match self {
124 DdlOperation::CreateTable {
125 table,
126 if_not_exists,
127 } => {
128 if *if_not_exists {
129 write!(f, "CREATE TABLE IF NOT EXISTS {}", table)
130 } else {
131 write!(f, "CREATE TABLE {}", table)
132 }
133 }
134 DdlOperation::DropTable { table } => write!(f, "DROP TABLE {}", table),
135 DdlOperation::AlterTableAddColumn {
136 table,
137 column,
138 data_type,
139 ..
140 } => {
141 write!(
142 f,
143 "ALTER TABLE {} ADD COLUMN {} {}",
144 table, column, data_type
145 )
146 }
147 DdlOperation::AlterTableDropColumn { table, column } => {
148 write!(f, "ALTER TABLE {} DROP COLUMN {}", table, column)
149 }
150 DdlOperation::AlterTableAlterColumn { table, column } => {
151 write!(f, "ALTER TABLE {} ALTER COLUMN {}", table, column)
152 }
153 DdlOperation::CreateIndex {
154 name,
155 table,
156 is_unique,
157 is_concurrent,
158 } => {
159 let unique = if *is_unique { "UNIQUE " } else { "" };
160 let concurrent = if *is_concurrent { "CONCURRENTLY " } else { "" };
161 write!(
162 f,
163 "CREATE {}{}INDEX {} ON {}",
164 unique, concurrent, name, table
165 )
166 }
167 DdlOperation::DropIndex { name } => write!(f, "DROP INDEX {}", name),
168 DdlOperation::CreateView {
169 name,
170 is_materialized,
171 } => {
172 if *is_materialized {
173 write!(f, "CREATE MATERIALIZED VIEW {}", name)
174 } else {
175 write!(f, "CREATE VIEW {}", name)
176 }
177 }
178 DdlOperation::DropView { name } => write!(f, "DROP VIEW {}", name),
179 DdlOperation::CreateFunction { name } => write!(f, "CREATE FUNCTION {}", name),
180 DdlOperation::DropFunction { name } => write!(f, "DROP FUNCTION {}", name),
181 DdlOperation::AddConstraint {
182 table,
183 constraint_type,
184 } => {
185 write!(
186 f,
187 "ALTER TABLE {} ADD {} CONSTRAINT",
188 table, constraint_type
189 )
190 }
191 DdlOperation::DropConstraint { table, name } => {
192 write!(f, "ALTER TABLE {} DROP CONSTRAINT {}", table, name)
193 }
194 DdlOperation::CreateEnum { name } => write!(f, "CREATE TYPE {} AS ENUM", name),
195 DdlOperation::TruncateTable { table } => write!(f, "TRUNCATE TABLE {}", table),
196 DdlOperation::Other { statement_preview } => write!(f, "{}", statement_preview),
197 }
198 }
199}
200
201static CREATE_TABLE_RE: LazyLock<Regex> = LazyLock::new(|| {
203 Regex::new(r"(?i)CREATE\s+TABLE\s+(IF\s+NOT\s+EXISTS\s+)?(?:(\w+)\.)?(\w+)").unwrap()
204});
205
206static DROP_TABLE_RE: LazyLock<Regex> = LazyLock::new(|| {
207 Regex::new(r"(?i)DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:(\w+)\.)?(\w+)").unwrap()
208});
209
210static ALTER_TABLE_ADD_COLUMN_RE: LazyLock<Regex> = LazyLock::new(|| {
211 Regex::new(
212 r"(?i)ALTER\s+TABLE\s+(?:(\w+)\.)?(\w+)\s+ADD\s+(?:COLUMN\s+)?(\w+)\s+(\w[\w\s\(\),]*)",
213 )
214 .unwrap()
215});
216
217static ALTER_TABLE_DROP_COLUMN_RE: LazyLock<Regex> = LazyLock::new(|| {
218 Regex::new(
219 r"(?i)ALTER\s+TABLE\s+(?:(\w+)\.)?(\w+)\s+DROP\s+(?:COLUMN\s+)?(?:IF\s+EXISTS\s+)?(\w+)",
220 )
221 .unwrap()
222});
223
224static ALTER_TABLE_ALTER_COLUMN_RE: LazyLock<Regex> = LazyLock::new(|| {
225 Regex::new(r"(?i)ALTER\s+TABLE\s+(?:(\w+)\.)?(\w+)\s+ALTER\s+(?:COLUMN\s+)?(\w+)").unwrap()
226});
227
228static CREATE_INDEX_RE: LazyLock<Regex> = LazyLock::new(|| {
229 Regex::new(r"(?i)CREATE\s+(UNIQUE\s+)?INDEX\s+(CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+ON\s+(?:(\w+)\.)?(\w+)").unwrap()
230});
231
232static DROP_INDEX_RE: LazyLock<Regex> = LazyLock::new(|| {
233 Regex::new(r"(?i)DROP\s+INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+EXISTS\s+)?(?:(\w+)\.)?(\w+)")
234 .unwrap()
235});
236
237static CREATE_VIEW_RE: LazyLock<Regex> = LazyLock::new(|| {
238 Regex::new(r"(?i)CREATE\s+(?:OR\s+REPLACE\s+)?(MATERIALIZED\s+)?VIEW\s+(?:(\w+)\.)?(\w+)")
239 .unwrap()
240});
241
242static DROP_VIEW_RE: LazyLock<Regex> = LazyLock::new(|| {
243 Regex::new(r"(?i)DROP\s+(MATERIALIZED\s+)?VIEW\s+(?:IF\s+EXISTS\s+)?(?:(\w+)\.)?(\w+)").unwrap()
244});
245
246static CREATE_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {
247 Regex::new(r"(?i)CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(?:(\w+)\.)?(\w+)").unwrap()
248});
249
250static DROP_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {
251 Regex::new(r"(?i)DROP\s+FUNCTION\s+(?:IF\s+EXISTS\s+)?(?:(\w+)\.)?(\w+)").unwrap()
252});
253
254static ADD_CONSTRAINT_RE: LazyLock<Regex> = LazyLock::new(|| {
255 Regex::new(r"(?i)ALTER\s+TABLE\s+(?:(\w+)\.)?(\w+)\s+ADD\s+(?:CONSTRAINT\s+\w+\s+)?(PRIMARY\s+KEY|UNIQUE|FOREIGN\s+KEY|CHECK|EXCLUDE)").unwrap()
256});
257
258static DROP_CONSTRAINT_RE: LazyLock<Regex> = LazyLock::new(|| {
259 Regex::new(
260 r"(?i)ALTER\s+TABLE\s+(?:(\w+)\.)?(\w+)\s+DROP\s+CONSTRAINT\s+(?:IF\s+EXISTS\s+)?(\w+)",
261 )
262 .unwrap()
263});
264
265static CREATE_ENUM_RE: LazyLock<Regex> =
266 LazyLock::new(|| Regex::new(r"(?i)CREATE\s+TYPE\s+(?:(\w+)\.)?(\w+)\s+AS\s+ENUM").unwrap());
267
268static TRUNCATE_RE: LazyLock<Regex> =
269 LazyLock::new(|| Regex::new(r"(?i)TRUNCATE\s+(?:TABLE\s+)?(?:(\w+)\.)?(\w+)").unwrap());
270
271pub fn extract_ddl_operations(sql: &str) -> Vec<DdlOperation> {
273 let statements = split_statements(sql);
274 let mut ops = Vec::new();
275
276 for stmt in statements {
277 let trimmed = stmt.trim();
278 if trimmed.is_empty() {
279 continue;
280 }
281
282 if let Some(op) = parse_statement(trimmed) {
283 ops.push(op);
284 }
285 }
286
287 ops
288}
289
290fn parse_statement(stmt: &str) -> Option<DdlOperation> {
291 if let Some(caps) = ADD_CONSTRAINT_RE.captures(stmt) {
295 let table = caps.get(2).unwrap().as_str().to_string();
296 let constraint_type = caps.get(3).unwrap().as_str().to_uppercase();
297 return Some(DdlOperation::AddConstraint {
298 table,
299 constraint_type,
300 });
301 }
302
303 if let Some(caps) = DROP_CONSTRAINT_RE.captures(stmt) {
305 let table = caps.get(2).unwrap().as_str().to_string();
306 let name = caps.get(3).unwrap().as_str().to_string();
307 return Some(DdlOperation::DropConstraint { table, name });
308 }
309
310 if ALTER_TABLE_ALTER_COLUMN_RE.is_match(stmt) {
312 if let Some(caps) = ALTER_TABLE_ALTER_COLUMN_RE.captures(stmt) {
313 let table = caps.get(2).unwrap().as_str().to_string();
314 let column = caps.get(3).unwrap().as_str().to_string();
315 return Some(DdlOperation::AlterTableAlterColumn { table, column });
316 }
317 }
318
319 if let Some(caps) = ALTER_TABLE_DROP_COLUMN_RE.captures(stmt) {
321 let table = caps.get(2).unwrap().as_str().to_string();
322 let column = caps.get(3).unwrap().as_str().to_string();
323 return Some(DdlOperation::AlterTableDropColumn { table, column });
324 }
325
326 if let Some(caps) = ALTER_TABLE_ADD_COLUMN_RE.captures(stmt) {
328 let table = caps.get(2).unwrap().as_str().to_string();
329 let column = caps.get(3).unwrap().as_str().to_string();
330 let rest = caps.get(4).unwrap().as_str();
331 let data_type = rest
333 .split_whitespace()
334 .next()
335 .unwrap_or("unknown")
336 .to_string();
337 let upper = stmt.to_uppercase();
338 let has_default = upper.contains("DEFAULT");
339 let is_not_null = upper.contains("NOT NULL");
340 return Some(DdlOperation::AlterTableAddColumn {
341 table,
342 column,
343 data_type,
344 has_default,
345 is_not_null,
346 });
347 }
348
349 if let Some(caps) = CREATE_TABLE_RE.captures(stmt) {
351 let if_not_exists = caps.get(1).is_some();
352 let table = caps.get(3).unwrap().as_str().to_string();
353 return Some(DdlOperation::CreateTable {
354 table,
355 if_not_exists,
356 });
357 }
358
359 if let Some(caps) = DROP_TABLE_RE.captures(stmt) {
361 let table = caps.get(2).unwrap().as_str().to_string();
362 return Some(DdlOperation::DropTable { table });
363 }
364
365 if let Some(caps) = CREATE_INDEX_RE.captures(stmt) {
367 let is_unique = caps.get(1).is_some();
368 let is_concurrent = caps.get(2).is_some();
369 let name = caps.get(3).unwrap().as_str().to_string();
370 let table = caps.get(5).unwrap().as_str().to_string();
371 return Some(DdlOperation::CreateIndex {
372 name,
373 table,
374 is_concurrent,
375 is_unique,
376 });
377 }
378
379 if let Some(caps) = DROP_INDEX_RE.captures(stmt) {
381 let name = caps.get(2).unwrap().as_str().to_string();
382 return Some(DdlOperation::DropIndex { name });
383 }
384
385 if let Some(caps) = CREATE_VIEW_RE.captures(stmt) {
387 let is_materialized = caps.get(1).is_some();
388 let name = caps.get(3).unwrap().as_str().to_string();
389 return Some(DdlOperation::CreateView {
390 name,
391 is_materialized,
392 });
393 }
394
395 if let Some(caps) = DROP_VIEW_RE.captures(stmt) {
397 let name = caps.get(3).unwrap().as_str().to_string();
398 return Some(DdlOperation::DropView { name });
399 }
400
401 if let Some(caps) = CREATE_FUNCTION_RE.captures(stmt) {
403 let name = caps.get(2).unwrap().as_str().to_string();
404 return Some(DdlOperation::CreateFunction { name });
405 }
406
407 if let Some(caps) = DROP_FUNCTION_RE.captures(stmt) {
409 let name = caps.get(2).unwrap().as_str().to_string();
410 return Some(DdlOperation::DropFunction { name });
411 }
412
413 if let Some(caps) = CREATE_ENUM_RE.captures(stmt) {
415 let name = caps.get(2).unwrap().as_str().to_string();
416 return Some(DdlOperation::CreateEnum { name });
417 }
418
419 if let Some(caps) = TRUNCATE_RE.captures(stmt) {
421 let table = caps.get(2).unwrap().as_str().to_string();
422 return Some(DdlOperation::TruncateTable { table });
423 }
424
425 let upper = stmt.trim_start().to_uppercase();
427 if upper.starts_with("--") || upper.is_empty() {
428 return None;
429 }
430
431 let preview: String = stmt.chars().take(80).collect();
433 let preview = if stmt.len() > 80 {
434 format!("{}...", preview)
435 } else {
436 preview
437 };
438 Some(DdlOperation::Other {
439 statement_preview: preview,
440 })
441}
442
443pub fn split_statements(sql: &str) -> Vec<&str> {
446 let bytes = sql.as_bytes();
447 let len = bytes.len();
448 let mut statements = Vec::new();
449 let mut start = 0;
450 let mut i = 0;
451
452 while i < len {
453 match bytes[i] {
454 b'-' if i + 1 < len && bytes[i + 1] == b'-' => {
456 while i < len && bytes[i] != b'\n' {
458 i += 1;
459 }
460 }
461 b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
463 i += 2;
464 let mut depth = 1;
465 while i < len && depth > 0 {
466 if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
467 depth += 1;
468 i += 2;
469 } else if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
470 depth -= 1;
471 i += 2;
472 } else {
473 i += 1;
474 }
475 }
476 continue;
477 }
478 b'\'' => {
480 let is_escape_string = i > 0
482 && (bytes[i - 1] == b'E' || bytes[i - 1] == b'e')
483 && (i < 2 || !(bytes[i - 2].is_ascii_alphanumeric() || bytes[i - 2] == b'_'));
484 i += 1;
485 while i < len {
486 if is_escape_string && bytes[i] == b'\\' {
487 i += 2; continue;
489 }
490 if bytes[i] == b'\'' {
491 if i + 1 < len && bytes[i + 1] == b'\'' {
492 i += 2; } else {
494 i += 1;
495 break;
496 }
497 } else {
498 i += 1;
499 }
500 }
501 continue;
502 }
503 b'$' => {
505 let tag_start = i;
507 i += 1;
508 while i < len && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
509 i += 1;
510 }
511 if i < len && bytes[i] == b'$' {
512 let tag = &sql[tag_start..=i];
513 i += 1;
514 loop {
516 if i >= len {
517 break;
518 }
519 if bytes[i] == b'$' {
520 let remaining = &sql[i..];
521 if remaining.starts_with(tag) {
522 i += tag.len();
523 break;
524 }
525 }
526 i += 1;
527 }
528 }
529 continue;
530 }
531 b';' => {
533 let stmt = &sql[start..i];
534 let trimmed = stmt.trim();
535 if !trimmed.is_empty() {
536 statements.push(trimmed);
537 }
538 i += 1;
539 start = i;
540 continue;
541 }
542 _ => {}
543 }
544 i += 1;
545 }
546
547 let remainder = sql[start..].trim();
549 if !remainder.is_empty() {
550 statements.push(remainder);
551 }
552
553 statements
554}
555
556pub fn line_number_at(sql: &str, offset: usize) -> usize {
558 sql[..offset.min(sql.len())].lines().count()
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
566 fn test_split_simple_statements() {
567 let sql = "SELECT 1; SELECT 2;";
568 let stmts = split_statements(sql);
569 assert_eq!(stmts, vec!["SELECT 1", "SELECT 2"]);
570 }
571
572 #[test]
573 fn test_split_respects_string_literals() {
574 let sql = "SELECT 'hello;world'; SELECT 2;";
575 let stmts = split_statements(sql);
576 assert_eq!(stmts, vec!["SELECT 'hello;world'", "SELECT 2"]);
577 }
578
579 #[test]
580 fn test_split_respects_dollar_quoting() {
581 let sql =
582 "CREATE FUNCTION foo() RETURNS void AS $$ BEGIN; END; $$ LANGUAGE plpgsql; SELECT 1;";
583 let stmts = split_statements(sql);
584 assert_eq!(stmts.len(), 2);
585 assert!(stmts[0].contains("BEGIN; END;"));
586 }
587
588 #[test]
589 fn test_split_respects_tagged_dollar_quoting() {
590 let sql = "CREATE FUNCTION foo() RETURNS void AS $body$ BEGIN; END; $body$ LANGUAGE plpgsql; SELECT 1;";
591 let stmts = split_statements(sql);
592 assert_eq!(stmts.len(), 2);
593 assert!(stmts[0].contains("BEGIN; END;"));
594 }
595
596 #[test]
597 fn test_split_respects_comments() {
598 let sql = "-- This is a comment with ; semicolon\nSELECT 1;";
599 let stmts = split_statements(sql);
600 assert_eq!(stmts.len(), 1);
601 }
602
603 #[test]
604 fn test_split_no_trailing_semicolon() {
605 let sql = "SELECT 1";
606 let stmts = split_statements(sql);
607 assert_eq!(stmts, vec!["SELECT 1"]);
608 }
609
610 #[test]
611 fn test_extract_create_table() {
612 let sql = "CREATE TABLE users (id SERIAL PRIMARY KEY);";
613 let ops = extract_ddl_operations(sql);
614 assert_eq!(ops.len(), 1);
615 match &ops[0] {
616 DdlOperation::CreateTable {
617 table,
618 if_not_exists,
619 } => {
620 assert_eq!(table, "users");
621 assert!(!if_not_exists);
622 }
623 _ => panic!("Expected CreateTable"),
624 }
625 }
626
627 #[test]
628 fn test_extract_create_table_if_not_exists() {
629 let sql = "CREATE TABLE IF NOT EXISTS users (id SERIAL);";
630 let ops = extract_ddl_operations(sql);
631 match &ops[0] {
632 DdlOperation::CreateTable {
633 table,
634 if_not_exists,
635 } => {
636 assert_eq!(table, "users");
637 assert!(if_not_exists);
638 }
639 _ => panic!("Expected CreateTable"),
640 }
641 }
642
643 #[test]
644 fn test_extract_add_column() {
645 let sql = "ALTER TABLE users ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';";
646 let ops = extract_ddl_operations(sql);
647 match &ops[0] {
648 DdlOperation::AlterTableAddColumn {
649 table,
650 column,
651 is_not_null,
652 has_default,
653 ..
654 } => {
655 assert_eq!(table, "users");
656 assert_eq!(column, "email");
657 assert!(is_not_null);
658 assert!(has_default);
659 }
660 _ => panic!("Expected AlterTableAddColumn"),
661 }
662 }
663
664 #[test]
665 fn test_extract_create_index() {
666 let sql = "CREATE UNIQUE INDEX CONCURRENTLY idx_users_email ON users (email);";
667 let ops = extract_ddl_operations(sql);
668 match &ops[0] {
669 DdlOperation::CreateIndex {
670 name,
671 table,
672 is_concurrent,
673 is_unique,
674 } => {
675 assert_eq!(name, "idx_users_email");
676 assert_eq!(table, "users");
677 assert!(is_concurrent);
678 assert!(is_unique);
679 }
680 _ => panic!("Expected CreateIndex"),
681 }
682 }
683
684 #[test]
685 fn test_extract_create_function() {
686 let sql = "CREATE OR REPLACE FUNCTION my_func() RETURNS void AS $$ BEGIN END; $$ LANGUAGE plpgsql;";
687 let ops = extract_ddl_operations(sql);
688 match &ops[0] {
689 DdlOperation::CreateFunction { name } => {
690 assert_eq!(name, "my_func");
691 }
692 _ => panic!("Expected CreateFunction, got {:?}", ops[0]),
693 }
694 }
695
696 #[test]
697 fn test_extract_create_enum() {
698 let sql = "CREATE TYPE mood AS ENUM ('happy', 'sad');";
699 let ops = extract_ddl_operations(sql);
700 match &ops[0] {
701 DdlOperation::CreateEnum { name } => {
702 assert_eq!(name, "mood");
703 }
704 _ => panic!("Expected CreateEnum"),
705 }
706 }
707
708 #[test]
709 fn test_extract_multiple() {
710 let sql = "CREATE TABLE users (id SERIAL); CREATE INDEX idx_users ON users (id); DROP TABLE old_table;";
711 let ops = extract_ddl_operations(sql);
712 assert_eq!(ops.len(), 3);
713 }
714
715 #[test]
716 fn test_extract_truncate() {
717 let sql = "TRUNCATE TABLE users;";
718 let ops = extract_ddl_operations(sql);
719 match &ops[0] {
720 DdlOperation::TruncateTable { table } => assert_eq!(table, "users"),
721 _ => panic!("Expected TruncateTable"),
722 }
723 }
724
725 #[test]
726 fn test_extract_drop_column() {
727 let sql = "ALTER TABLE users DROP COLUMN email;";
728 let ops = extract_ddl_operations(sql);
729 match &ops[0] {
730 DdlOperation::AlterTableDropColumn { table, column } => {
731 assert_eq!(table, "users");
732 assert_eq!(column, "email");
733 }
734 _ => panic!("Expected AlterTableDropColumn"),
735 }
736 }
737
738 #[test]
739 fn test_extract_alter_column() {
740 let sql = "ALTER TABLE users ALTER COLUMN name TYPE text;";
741 let ops = extract_ddl_operations(sql);
742 match &ops[0] {
743 DdlOperation::AlterTableAlterColumn { table, column } => {
744 assert_eq!(table, "users");
745 assert_eq!(column, "name");
746 }
747 _ => panic!("Expected AlterTableAlterColumn"),
748 }
749 }
750
751 #[test]
752 fn test_extract_materialized_view() {
753 let sql = "CREATE MATERIALIZED VIEW user_stats AS SELECT count(*) FROM users;";
754 let ops = extract_ddl_operations(sql);
755 match &ops[0] {
756 DdlOperation::CreateView {
757 name,
758 is_materialized,
759 } => {
760 assert_eq!(name, "user_stats");
761 assert!(is_materialized);
762 }
763 _ => panic!("Expected CreateView"),
764 }
765 }
766
767 #[test]
768 fn test_block_comment_with_semicolons() {
769 let sql = "/* comment; with; semicolons */ SELECT 1;";
770 let stmts = split_statements(sql);
771 assert_eq!(stmts.len(), 1);
772 }
773
774 #[test]
775 fn test_escaped_string_quotes() {
776 let sql = "SELECT 'it''s; here'; SELECT 2;";
777 let stmts = split_statements(sql);
778 assert_eq!(stmts.len(), 2);
779 }
780
781 #[test]
782 fn test_split_respects_e_escape_strings() {
783 let sql = r"SELECT E'hello\';world'; SELECT 2;";
784 let stmts = split_statements(sql);
785 assert_eq!(stmts.len(), 2);
786 assert!(stmts[0].contains(r"E'hello\';world'"));
787 }
788
789 #[test]
790 fn test_split_e_string_with_backslash() {
791 let sql = r"SELECT E'it\'s a test; really'; SELECT 1;";
792 let stmts = split_statements(sql);
793 assert_eq!(stmts.len(), 2);
794 }
795
796 #[test]
797 fn test_split_nested_block_comments() {
798 let sql = "SELECT /* outer /* inner */ outer */ 1; SELECT 2;";
799 let stmts = split_statements(sql);
800 assert_eq!(stmts.len(), 2);
801 assert_eq!(stmts[1], "SELECT 2");
802 }
803
804 #[test]
805 fn test_split_whitespace_only() {
806 let stmts = split_statements(" \n\t ");
807 assert!(stmts.is_empty());
808 }
809
810 #[test]
811 fn test_split_comment_only() {
812 let stmts = split_statements("-- just a comment\n");
813 assert_eq!(stmts.len(), 1);
814 assert_eq!(stmts[0], "-- just a comment");
815 }
816
817 #[test]
818 fn test_split_mixed_e_and_regular_strings() {
819 let sql = r"SELECT 'normal;string', E'escape\';string'; SELECT 2;";
820 let stmts = split_statements(sql);
821 assert_eq!(stmts.len(), 2);
822 }
823}