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
561pub fn split_mysql_statements(sql: &str) -> Vec<String> {
572 let bytes = sql.as_bytes();
573 let len = bytes.len();
574 let mut out = Vec::new();
575 let mut start = 0;
576 let mut i = 0;
577 while i < len {
578 let c = bytes[i];
579 if c == b'-' && i + 1 < len && bytes[i + 1] == b'-' {
581 while i < len && bytes[i] != b'\n' {
582 i += 1;
583 }
584 continue;
585 }
586 if c == b'/' && i + 1 < len && bytes[i + 1] == b'*' {
588 i += 2;
589 while i + 1 < len && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
590 i += 1;
591 }
592 i = (i + 2).min(len);
593 continue;
594 }
595 if c == b'\'' {
597 i += 1;
598 while i < len && bytes[i] != b'\'' {
599 if bytes[i] == b'\\' && i + 1 < len {
600 i += 2;
601 } else {
602 i += 1;
603 }
604 }
605 i += 1;
606 continue;
607 }
608 if c == b'"' {
610 i += 1;
611 while i < len && bytes[i] != b'"' {
612 if bytes[i] == b'\\' && i + 1 < len {
613 i += 2;
614 } else {
615 i += 1;
616 }
617 }
618 i += 1;
619 continue;
620 }
621 if c == b'`' {
623 i += 1;
624 while i < len && bytes[i] != b'`' {
625 i += 1;
626 }
627 i += 1;
628 continue;
629 }
630 if c == b';' {
632 out.push(sql[start..i].to_string());
633 i += 1;
634 start = i;
635 continue;
636 }
637 i += 1;
638 }
639 let tail = sql[start..].trim();
640 if !tail.is_empty() {
641 out.push(tail.to_string());
642 }
643 out
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649
650 #[test]
651 fn test_split_simple_statements() {
652 let sql = "SELECT 1; SELECT 2;";
653 let stmts = split_statements(sql);
654 assert_eq!(stmts, vec!["SELECT 1", "SELECT 2"]);
655 }
656
657 #[test]
658 fn test_split_respects_string_literals() {
659 let sql = "SELECT 'hello;world'; SELECT 2;";
660 let stmts = split_statements(sql);
661 assert_eq!(stmts, vec!["SELECT 'hello;world'", "SELECT 2"]);
662 }
663
664 #[test]
665 fn test_split_respects_dollar_quoting() {
666 let sql =
667 "CREATE FUNCTION foo() RETURNS void AS $$ BEGIN; END; $$ LANGUAGE plpgsql; SELECT 1;";
668 let stmts = split_statements(sql);
669 assert_eq!(stmts.len(), 2);
670 assert!(stmts[0].contains("BEGIN; END;"));
671 }
672
673 #[test]
674 fn test_split_respects_tagged_dollar_quoting() {
675 let sql = "CREATE FUNCTION foo() RETURNS void AS $body$ BEGIN; END; $body$ LANGUAGE plpgsql; SELECT 1;";
676 let stmts = split_statements(sql);
677 assert_eq!(stmts.len(), 2);
678 assert!(stmts[0].contains("BEGIN; END;"));
679 }
680
681 #[test]
682 fn test_split_respects_comments() {
683 let sql = "-- This is a comment with ; semicolon\nSELECT 1;";
684 let stmts = split_statements(sql);
685 assert_eq!(stmts.len(), 1);
686 }
687
688 #[test]
689 fn test_split_no_trailing_semicolon() {
690 let sql = "SELECT 1";
691 let stmts = split_statements(sql);
692 assert_eq!(stmts, vec!["SELECT 1"]);
693 }
694
695 #[test]
696 fn test_extract_create_table() {
697 let sql = "CREATE TABLE users (id SERIAL PRIMARY KEY);";
698 let ops = extract_ddl_operations(sql);
699 assert_eq!(ops.len(), 1);
700 match &ops[0] {
701 DdlOperation::CreateTable {
702 table,
703 if_not_exists,
704 } => {
705 assert_eq!(table, "users");
706 assert!(!if_not_exists);
707 }
708 _ => panic!("Expected CreateTable"),
709 }
710 }
711
712 #[test]
713 fn test_extract_create_table_if_not_exists() {
714 let sql = "CREATE TABLE IF NOT EXISTS users (id SERIAL);";
715 let ops = extract_ddl_operations(sql);
716 match &ops[0] {
717 DdlOperation::CreateTable {
718 table,
719 if_not_exists,
720 } => {
721 assert_eq!(table, "users");
722 assert!(if_not_exists);
723 }
724 _ => panic!("Expected CreateTable"),
725 }
726 }
727
728 #[test]
729 fn test_extract_add_column() {
730 let sql = "ALTER TABLE users ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';";
731 let ops = extract_ddl_operations(sql);
732 match &ops[0] {
733 DdlOperation::AlterTableAddColumn {
734 table,
735 column,
736 is_not_null,
737 has_default,
738 ..
739 } => {
740 assert_eq!(table, "users");
741 assert_eq!(column, "email");
742 assert!(is_not_null);
743 assert!(has_default);
744 }
745 _ => panic!("Expected AlterTableAddColumn"),
746 }
747 }
748
749 #[test]
750 fn test_extract_create_index() {
751 let sql = "CREATE UNIQUE INDEX CONCURRENTLY idx_users_email ON users (email);";
752 let ops = extract_ddl_operations(sql);
753 match &ops[0] {
754 DdlOperation::CreateIndex {
755 name,
756 table,
757 is_concurrent,
758 is_unique,
759 } => {
760 assert_eq!(name, "idx_users_email");
761 assert_eq!(table, "users");
762 assert!(is_concurrent);
763 assert!(is_unique);
764 }
765 _ => panic!("Expected CreateIndex"),
766 }
767 }
768
769 #[test]
770 fn test_extract_create_function() {
771 let sql = "CREATE OR REPLACE FUNCTION my_func() RETURNS void AS $$ BEGIN END; $$ LANGUAGE plpgsql;";
772 let ops = extract_ddl_operations(sql);
773 match &ops[0] {
774 DdlOperation::CreateFunction { name } => {
775 assert_eq!(name, "my_func");
776 }
777 _ => panic!("Expected CreateFunction, got {:?}", ops[0]),
778 }
779 }
780
781 #[test]
782 fn test_extract_create_enum() {
783 let sql = "CREATE TYPE mood AS ENUM ('happy', 'sad');";
784 let ops = extract_ddl_operations(sql);
785 match &ops[0] {
786 DdlOperation::CreateEnum { name } => {
787 assert_eq!(name, "mood");
788 }
789 _ => panic!("Expected CreateEnum"),
790 }
791 }
792
793 #[test]
794 fn test_extract_multiple() {
795 let sql = "CREATE TABLE users (id SERIAL); CREATE INDEX idx_users ON users (id); DROP TABLE old_table;";
796 let ops = extract_ddl_operations(sql);
797 assert_eq!(ops.len(), 3);
798 }
799
800 #[test]
801 fn test_extract_truncate() {
802 let sql = "TRUNCATE TABLE users;";
803 let ops = extract_ddl_operations(sql);
804 match &ops[0] {
805 DdlOperation::TruncateTable { table } => assert_eq!(table, "users"),
806 _ => panic!("Expected TruncateTable"),
807 }
808 }
809
810 #[test]
811 fn test_extract_drop_column() {
812 let sql = "ALTER TABLE users DROP COLUMN email;";
813 let ops = extract_ddl_operations(sql);
814 match &ops[0] {
815 DdlOperation::AlterTableDropColumn { table, column } => {
816 assert_eq!(table, "users");
817 assert_eq!(column, "email");
818 }
819 _ => panic!("Expected AlterTableDropColumn"),
820 }
821 }
822
823 #[test]
824 fn test_extract_alter_column() {
825 let sql = "ALTER TABLE users ALTER COLUMN name TYPE text;";
826 let ops = extract_ddl_operations(sql);
827 match &ops[0] {
828 DdlOperation::AlterTableAlterColumn { table, column } => {
829 assert_eq!(table, "users");
830 assert_eq!(column, "name");
831 }
832 _ => panic!("Expected AlterTableAlterColumn"),
833 }
834 }
835
836 #[test]
837 fn test_extract_materialized_view() {
838 let sql = "CREATE MATERIALIZED VIEW user_stats AS SELECT count(*) FROM users;";
839 let ops = extract_ddl_operations(sql);
840 match &ops[0] {
841 DdlOperation::CreateView {
842 name,
843 is_materialized,
844 } => {
845 assert_eq!(name, "user_stats");
846 assert!(is_materialized);
847 }
848 _ => panic!("Expected CreateView"),
849 }
850 }
851
852 #[test]
853 fn test_block_comment_with_semicolons() {
854 let sql = "/* comment; with; semicolons */ SELECT 1;";
855 let stmts = split_statements(sql);
856 assert_eq!(stmts.len(), 1);
857 }
858
859 #[test]
860 fn test_escaped_string_quotes() {
861 let sql = "SELECT 'it''s; here'; SELECT 2;";
862 let stmts = split_statements(sql);
863 assert_eq!(stmts.len(), 2);
864 }
865
866 #[test]
867 fn test_split_respects_e_escape_strings() {
868 let sql = r"SELECT E'hello\';world'; SELECT 2;";
869 let stmts = split_statements(sql);
870 assert_eq!(stmts.len(), 2);
871 assert!(stmts[0].contains(r"E'hello\';world'"));
872 }
873
874 #[test]
875 fn test_split_e_string_with_backslash() {
876 let sql = r"SELECT E'it\'s a test; really'; SELECT 1;";
877 let stmts = split_statements(sql);
878 assert_eq!(stmts.len(), 2);
879 }
880
881 #[test]
882 fn test_split_nested_block_comments() {
883 let sql = "SELECT /* outer /* inner */ outer */ 1; SELECT 2;";
884 let stmts = split_statements(sql);
885 assert_eq!(stmts.len(), 2);
886 assert_eq!(stmts[1], "SELECT 2");
887 }
888
889 #[test]
890 fn test_split_whitespace_only() {
891 let stmts = split_statements(" \n\t ");
892 assert!(stmts.is_empty());
893 }
894
895 #[test]
896 fn test_split_comment_only() {
897 let stmts = split_statements("-- just a comment\n");
898 assert_eq!(stmts.len(), 1);
899 assert_eq!(stmts[0], "-- just a comment");
900 }
901
902 #[test]
903 fn test_split_mixed_e_and_regular_strings() {
904 let sql = r"SELECT 'normal;string', E'escape\';string'; SELECT 2;";
905 let stmts = split_statements(sql);
906 assert_eq!(stmts.len(), 2);
907 }
908
909 #[test]
910 fn test_split_mysql_basic() {
911 let sql = "CREATE TABLE a (id INT); CREATE TABLE b (id INT);";
912 let stmts = split_mysql_statements(sql);
913 assert_eq!(stmts.len(), 2);
914 assert!(stmts[0].contains("CREATE TABLE a"));
915 assert!(stmts[1].contains("CREATE TABLE b"));
916 }
917
918 #[test]
919 fn test_split_mysql_respects_backticks_with_semicolons() {
920 let sql = "CREATE TABLE `weird;name` (id INT); CREATE TABLE b (id INT);";
922 let stmts = split_mysql_statements(sql);
923 assert_eq!(stmts.len(), 2);
924 assert!(stmts[0].contains("`weird;name`"));
925 }
926
927 #[test]
928 fn test_split_mysql_respects_string_literals_with_semicolons() {
929 let sql = "INSERT INTO t VALUES ('a;b'); INSERT INTO t VALUES ('c;d');";
930 let stmts = split_mysql_statements(sql);
931 assert_eq!(stmts.len(), 2);
932 }
933
934 #[test]
935 fn test_split_mysql_keeps_leading_comments_with_statement() {
936 let sql = "-- header comment\nCREATE TABLE a (id INT);\nCREATE TABLE b (id INT);";
939 let stmts = split_mysql_statements(sql);
940 assert_eq!(stmts.len(), 2);
941 assert!(stmts[0].contains("CREATE TABLE a"));
942 }
943
944 #[test]
945 fn test_split_mysql_handles_block_comments() {
946 let sql = "/* block ; comment */ CREATE TABLE a (id INT); CREATE TABLE b (id INT);";
947 let stmts = split_mysql_statements(sql);
948 assert_eq!(stmts.len(), 2);
949 }
950
951 #[test]
952 fn test_split_mysql_no_trailing_semicolon() {
953 let sql = "CREATE TABLE a (id INT)";
954 let stmts = split_mysql_statements(sql);
955 assert_eq!(stmts.len(), 1);
956 assert!(stmts[0].contains("CREATE TABLE a"));
957 }
958}