1use serde::{Deserialize, Serialize};
36use sqlx::{PgPool, Row};
37use std::collections::HashMap;
38use std::fmt;
39
40fn parse_postgres_array(array_str: &str) -> Vec<String> {
42 if array_str.is_empty() || array_str == "{}" {
43 return vec![];
44 }
45
46 let trimmed = array_str.trim_start_matches('{').trim_end_matches('}');
48 if trimmed.is_empty() {
49 return vec![];
50 }
51
52 trimmed
53 .split(',')
54 .map(|s| s.trim().trim_matches('"').to_string())
55 .collect()
56}
57
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60pub enum DefaultValue {
61 String(String),
63 Integer(i64),
65 Float(f64),
67 Boolean(bool),
69 Null,
71 CurrentTimestamp,
73 CurrentDate,
75 CurrentTime,
77 Expression(String),
79 Binary(String),
81}
82
83impl fmt::Display for DefaultValue {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 match self {
86 DefaultValue::String(s) => write!(f, "String: \"{}\"", s),
87 DefaultValue::Integer(i) => write!(f, "Integer: {}", i),
88 DefaultValue::Float(fl) => write!(f, "Float: {}", fl),
89 DefaultValue::Boolean(b) => write!(f, "Boolean: {}", b),
90 DefaultValue::Null => write!(f, "Null"),
91 DefaultValue::CurrentTimestamp => write!(f, "CURRENT_TIMESTAMP"),
92 DefaultValue::CurrentDate => write!(f, "CURRENT_DATE"),
93 DefaultValue::CurrentTime => write!(f, "CURRENT_TIME"),
94 DefaultValue::Expression(e) => write!(f, "Expression: {}", e),
95 DefaultValue::Binary(b) => write!(f, "Binary: {}", b),
96 }
97 }
98}
99
100impl DefaultValue {
101 pub fn to_ddl_string(&self) -> String {
103 match self {
104 DefaultValue::String(s) => {
105 let escaped = s.replace('\'', "''");
107 format!("'{}'", escaped)
108 }
109 DefaultValue::Integer(i) => i.to_string(),
110 DefaultValue::Float(f) => f.to_string(),
111 DefaultValue::Boolean(b) => b.to_string(),
112 DefaultValue::Null => "NULL".to_string(),
113 DefaultValue::CurrentTimestamp => "CURRENT_TIMESTAMP".to_string(),
114 DefaultValue::CurrentDate => "CURRENT_DATE".to_string(),
115 DefaultValue::CurrentTime => "CURRENT_TIME".to_string(),
116 DefaultValue::Expression(e) => e.clone(),
117 DefaultValue::Binary(b) => format!("'{}'::bytea", b),
118 }
119 }
120}
121
122pub fn parse_default_value(default_str: &str) -> DefaultValue {
168 let trimmed = default_str.trim();
169
170 if trimmed.eq_ignore_ascii_case("NULL") {
172 return DefaultValue::Null;
173 }
174
175 if trimmed.eq_ignore_ascii_case("true") {
177 return DefaultValue::Boolean(true);
178 }
179 if trimmed.eq_ignore_ascii_case("false") {
180 return DefaultValue::Boolean(false);
181 }
182
183 if trimmed.eq_ignore_ascii_case("CURRENT_TIMESTAMP") {
185 return DefaultValue::CurrentTimestamp;
186 }
187 if trimmed.eq_ignore_ascii_case("CURRENT_DATE") {
188 return DefaultValue::CurrentDate;
189 }
190 if trimmed.eq_ignore_ascii_case("CURRENT_TIME") {
191 return DefaultValue::CurrentTime;
192 }
193
194 if trimmed.starts_with('\'') {
196 return parse_string_literal(trimmed);
197 }
198
199 if let Ok(int_val) = trimmed.parse::<i64>() {
201 return DefaultValue::Integer(int_val);
202 }
203 if let Ok(float_val) = trimmed.parse::<f64>() {
204 return DefaultValue::Float(float_val);
205 }
206
207 DefaultValue::Expression(trimmed.to_string())
209}
210
211fn parse_string_literal(input: &str) -> DefaultValue {
218 let input = input.trim();
219
220 let mut chars = input.chars().peekable();
222 let mut result = String::new();
223 let mut type_cast = String::new();
224 let mut after_string = false;
225
226 if chars.next() != Some('\'') {
228 return DefaultValue::Expression(input.to_string());
229 }
230 let mut in_string = true;
231
232 while let Some(ch) = chars.next() {
233 if in_string {
234 if ch == '\'' {
235 if chars.peek() == Some(&'\'') {
237 result.push('\'');
239 chars.next(); } else {
241 in_string = false;
243 after_string = true;
244 }
245 } else {
246 result.push(ch);
247 }
248 } else if after_string {
249 type_cast.push(ch);
251 }
252 }
253
254 if type_cast.contains("bytea") {
256 return DefaultValue::Binary(result);
257 }
258
259 DefaultValue::String(result)
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
265pub enum GeometryType {
266 Point,
267 LineString,
268 Polygon,
269 MultiPoint,
270 MultiLineString,
271 MultiPolygon,
272 GeometryCollection,
273}
274
275impl fmt::Display for GeometryType {
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 let s = match self {
278 GeometryType::Point => "POINT",
279 GeometryType::LineString => "LINESTRING",
280 GeometryType::Polygon => "POLYGON",
281 GeometryType::MultiPoint => "MULTIPOINT",
282 GeometryType::MultiLineString => "MULTILINESTRING",
283 GeometryType::MultiPolygon => "MULTIPOLYGON",
284 GeometryType::GeometryCollection => "GEOMETRYCOLLECTION",
285 };
286 write!(f, "{}", s)
287 }
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
292pub struct GeometryInfo {
293 pub geometry_type: GeometryType,
294 pub srid: Option<i32>,
295 pub dimension: Option<i32>,
296}
297
298impl fmt::Display for GeometryInfo {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 match (&self.srid, &self.dimension) {
301 (Some(srid), Some(_dim)) => write!(f, "({}, {})", self.geometry_type, srid),
302 (Some(srid), None) => write!(f, "({}, {})", self.geometry_type, srid),
303 (None, Some(_dim)) => write!(f, "({})", self.geometry_type),
304 (None, None) => write!(f, "({})", self.geometry_type),
305 }
306 }
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
311pub enum PostgreSQLDataType {
312 SmallInt,
314 Integer,
315 BigInt,
316
317 SmallSerial,
319 Serial,
320 BigSerial,
321
322 Real,
324 DoublePrecision,
325
326 Numeric(Option<i32>, Option<i32>), Decimal(Option<i32>, Option<i32>), Char(Option<i32>),
332 Varchar(Option<i32>),
333 Text,
334
335 Bytea,
337
338 Date,
340 Time,
341 TimeWithTimeZone,
342 Timestamp,
343 TimestampWithTimeZone,
344 Interval,
345
346 Boolean,
348
349 Uuid,
351
352 Json,
354 Jsonb,
355
356 Array(Box<PostgreSQLDataType>),
358
359 Geometry(Option<GeometryInfo>),
361 Geography(Option<GeometryInfo>),
362
363 Custom(String),
365
366 Unknown(String),
368}
369
370impl fmt::Display for PostgreSQLDataType {
371 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372 match self {
373 PostgreSQLDataType::SmallInt => write!(f, "smallint"),
374 PostgreSQLDataType::Integer => write!(f, "integer"),
375 PostgreSQLDataType::BigInt => write!(f, "bigint"),
376 PostgreSQLDataType::SmallSerial => write!(f, "smallserial"),
377 PostgreSQLDataType::Serial => write!(f, "serial"),
378 PostgreSQLDataType::BigSerial => write!(f, "bigserial"),
379 PostgreSQLDataType::Real => write!(f, "real"),
380 PostgreSQLDataType::DoublePrecision => write!(f, "double precision"),
381 PostgreSQLDataType::Numeric(p, s) => match (p, s) {
382 (Some(precision), Some(scale)) => write!(f, "numeric({},{})", precision, scale),
383 (Some(precision), None) => write!(f, "numeric({})", precision),
384 _ => write!(f, "numeric"),
385 },
386 PostgreSQLDataType::Decimal(p, s) => match (p, s) {
387 (Some(precision), Some(scale)) => write!(f, "numeric({},{})", precision, scale),
388 (Some(precision), None) => write!(f, "numeric({})", precision),
389 _ => write!(f, "numeric"),
390 },
391 PostgreSQLDataType::Char(len) => match len {
392 Some(l) => write!(f, "char({})", l),
393 None => write!(f, "char"),
394 },
395 PostgreSQLDataType::Varchar(len) => match len {
396 Some(l) => write!(f, "varchar({})", l),
397 None => write!(f, "varchar"),
398 },
399 PostgreSQLDataType::Text => write!(f, "text"),
400 PostgreSQLDataType::Bytea => write!(f, "bytea"),
401 PostgreSQLDataType::Date => write!(f, "date"),
402 PostgreSQLDataType::Time => write!(f, "time"),
403 PostgreSQLDataType::TimeWithTimeZone => write!(f, "time with time zone"),
404 PostgreSQLDataType::Timestamp => write!(f, "timestamp"),
405 PostgreSQLDataType::TimestampWithTimeZone => write!(f, "timestamp with time zone"),
406 PostgreSQLDataType::Interval => write!(f, "interval"),
407 PostgreSQLDataType::Boolean => write!(f, "boolean"),
408 PostgreSQLDataType::Uuid => write!(f, "uuid"),
409 PostgreSQLDataType::Json => write!(f, "json"),
410 PostgreSQLDataType::Jsonb => write!(f, "jsonb"),
411 PostgreSQLDataType::Array(inner) => write!(f, "{}[]", inner),
412 PostgreSQLDataType::Geometry(info) => match info {
413 Some(geo_info) => write!(f, "geometry{}", geo_info),
414 None => write!(f, "geometry"),
415 },
416 PostgreSQLDataType::Geography(info) => match info {
417 Some(geo_info) => write!(f, "geography{}", geo_info),
418 None => write!(f, "geography"),
419 },
420 PostgreSQLDataType::Custom(type_name) => write!(f, "{}", type_name),
421 PostgreSQLDataType::Unknown(type_name) => write!(f, "{}", type_name),
422 }
423 }
424}
425
426impl PostgreSQLDataType {
427 pub fn to_ddl_string(&self) -> String {
429 match self {
430 PostgreSQLDataType::Array(inner) => match inner.as_ref() {
431 PostgreSQLDataType::Geometry(Some(info)) => {
432 format!("geometry{}[]", info)
433 }
434 PostgreSQLDataType::Geography(Some(info)) => {
435 format!("geography{}[]", info)
436 }
437 _ => format!("{}[]", inner.to_ddl_string()),
438 },
439 _ => self.to_string(),
440 }
441 }
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
446pub struct ColumnInfo {
447 pub column_name: String,
448 pub data_type: PostgreSQLDataType,
449 pub is_nullable: bool,
450 pub column_default: Option<DefaultValue>,
451 pub character_maximum_length: Option<i32>,
452 pub numeric_precision: Option<i32>,
453 pub numeric_scale: Option<i32>,
454 pub is_primary_key: bool,
455 pub comment: Option<String>,
456 pub collate: Option<String>,
457}
458
459impl ColumnInfo {
460 pub fn to_ddl(&self) -> String {
462 let mut ddl = format!(
463 "{} {}",
464 escape_db_identifier(&self.column_name),
465 self.data_type.to_ddl_string()
466 );
467
468 if let Some(collate) = &self.collate {
470 match &self.data_type {
471 PostgreSQLDataType::Char(_)
472 | PostgreSQLDataType::Varchar(_)
473 | PostgreSQLDataType::Text => {
474 ddl.push_str(&format!(" COLLATE \"{}\"", collate));
475 }
476 _ => {}
477 }
478 }
479
480 if !self.is_nullable {
481 ddl.push_str(" NOT NULL");
482 }
483
484 if let Some(default) = &self.column_default {
486 match &self.data_type {
487 PostgreSQLDataType::SmallSerial
488 | PostgreSQLDataType::Serial
489 | PostgreSQLDataType::BigSerial => {
490 }
492 _ => {
493 ddl.push_str(&format!(" DEFAULT {}", default.to_ddl_string()));
494 }
495 }
496 }
497
498 ddl
499 }
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
504pub enum ForeignKeyAction {
505 NoAction,
506 Restrict,
507 Cascade,
508 SetNull,
509 SetDefault,
510}
511
512impl ForeignKeyAction {
513 pub fn from_str(s: &str) -> Option<Self> {
515 match s.to_uppercase().as_str() {
516 "NO ACTION" => Some(ForeignKeyAction::NoAction),
517 "RESTRICT" => Some(ForeignKeyAction::Restrict),
518 "CASCADE" => Some(ForeignKeyAction::Cascade),
519 "SET NULL" => Some(ForeignKeyAction::SetNull),
520 "SET DEFAULT" => Some(ForeignKeyAction::SetDefault),
521 _ => None,
522 }
523 }
524
525 pub fn to_string(&self) -> String {
527 match self {
528 ForeignKeyAction::NoAction => "NO ACTION".to_string(),
529 ForeignKeyAction::Restrict => "RESTRICT".to_string(),
530 ForeignKeyAction::Cascade => "CASCADE".to_string(),
531 ForeignKeyAction::SetNull => "SET NULL".to_string(),
532 ForeignKeyAction::SetDefault => "SET DEFAULT".to_string(),
533 }
534 }
535}
536
537impl fmt::Display for ForeignKeyAction {
538 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
539 write!(f, "{}", self.to_string())
540 }
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
545pub enum IndexType {
546 BTree,
547 Hash,
548 Gin,
549 Gist,
550 SpGist,
551 Brin,
552 Bloom,
553}
554
555impl IndexType {
556 pub fn from_str(s: &str) -> Option<Self> {
558 match s.to_lowercase().as_str() {
559 "btree" => Some(IndexType::BTree),
560 "hash" => Some(IndexType::Hash),
561 "gin" => Some(IndexType::Gin),
562 "gist" => Some(IndexType::Gist),
563 "spgist" => Some(IndexType::SpGist),
564 "brin" => Some(IndexType::Brin),
565 "bloom" => Some(IndexType::Bloom),
566 _ => None,
567 }
568 }
569
570 pub fn to_string(&self) -> String {
572 match self {
573 IndexType::BTree => "btree".to_string(),
574 IndexType::Hash => "hash".to_string(),
575 IndexType::Gin => "gin".to_string(),
576 IndexType::Gist => "gist".to_string(),
577 IndexType::SpGist => "spgist".to_string(),
578 IndexType::Brin => "brin".to_string(),
579 IndexType::Bloom => "bloom".to_string(),
580 }
581 }
582}
583
584impl fmt::Display for IndexType {
585 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
586 write!(f, "{}", self.to_string())
587 }
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
592pub struct ForeignKeyConstraint {
593 pub constraint_name: String,
594 pub columns: Vec<String>,
595 pub referenced_table: String,
596 pub referenced_columns: Vec<String>,
597 pub on_delete: Option<ForeignKeyAction>,
598 pub on_update: Option<ForeignKeyAction>,
599 pub is_deferrable: bool,
600 pub initially_deferred: bool,
601}
602
603impl ForeignKeyConstraint {
604 pub fn to_ddl(&self) -> String {
606 let delete_action = match &self.on_delete {
607 Some(action) => format!(" ON DELETE {}", action),
608 None => String::new(),
609 };
610 let update_action = match &self.on_update {
611 Some(action) => format!(" ON UPDATE {}", action),
612 None => String::new(),
613 };
614 let deferrable = if self.is_deferrable {
615 if self.initially_deferred {
616 " DEFERRABLE INITIALLY DEFERRED"
617 } else {
618 " DEFERRABLE INITIALLY IMMEDIATE"
619 }
620 } else {
621 ""
622 };
623
624 format!(
625 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){}{}{}",
626 escape_db_identifier(&self.constraint_name),
627 self.columns
628 .iter()
629 .map(|v| escape_db_identifier(v))
630 .collect::<Vec<_>>()
631 .join(", "),
632 escape_db_identifier(&self.referenced_table),
633 self.referenced_columns
634 .iter()
635 .map(|v| escape_db_identifier(v))
636 .collect::<Vec<_>>()
637 .join(", "),
638 delete_action,
639 update_action,
640 deferrable
641 )
642 }
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
647pub struct IndexInfo {
648 pub index_name: String,
649 pub columns: Vec<String>,
650 pub is_unique: bool,
651 pub is_primary: bool,
652 pub index_type: IndexType,
653 pub condition: Option<String>, }
655
656impl IndexInfo {
657 pub fn to_ddl(&self, table_schema: &str, table_name: &str) -> String {
659 let unique_clause = if self.is_unique { "UNIQUE " } else { "" };
660 let using_clause = if self.index_type != IndexType::BTree {
661 format!(" USING {}", self.index_type.to_string().to_uppercase())
662 } else {
663 String::new()
664 };
665 let where_clause = match &self.condition {
666 Some(condition) => format!(" WHERE {}", condition),
667 None => String::new(),
668 };
669
670 format!(
671 "CREATE {}INDEX {} ON {}.{}{} ({}){}",
672 unique_clause,
673 escape_db_identifier(&self.index_name),
674 escape_db_identifier(&table_schema),
675 escape_db_identifier(&table_name),
676 using_clause,
677 self.columns
678 .iter()
679 .map(|v| escape_db_identifier(v))
680 .collect::<Vec<_>>()
681 .join(", "),
682 where_clause
683 )
684 }
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
689pub struct CheckConstraint {
690 pub constraint_name: String,
691 pub check_clause: String,
692}
693
694impl CheckConstraint {
695 pub fn to_ddl(&self) -> String {
697 format!(
698 "CONSTRAINT {} CHECK ({})",
699 escape_db_identifier(&self.constraint_name),
700 self.check_clause
701 )
702 }
703}
704
705#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
707pub struct TableInfo {
708 pub table_schema: String,
709 pub table_name: String,
710 pub columns: Vec<ColumnInfo>,
711 pub foreign_keys: Vec<ForeignKeyConstraint>,
712 pub indexes: Vec<IndexInfo>,
713 pub check_constraints: Vec<CheckConstraint>,
714 pub comment: Option<String>,
715}
716
717impl TableInfo {
718 pub fn uses_postgis(&self) -> bool {
720 self.columns.iter().any(|col| match &col.data_type {
721 PostgreSQLDataType::Geometry(_) | PostgreSQLDataType::Geography(_) => true,
722 PostgreSQLDataType::Array(inner) => matches!(
723 inner.as_ref(),
724 PostgreSQLDataType::Geometry(_) | PostgreSQLDataType::Geography(_)
725 ),
726 _ => false,
727 })
728 }
729
730 pub fn get_postgis_types(&self) -> HashMap<String, i32> {
732 let mut postgis_types = HashMap::new();
733
734 for col in &self.columns {
735 match &col.data_type {
736 PostgreSQLDataType::Geometry(_) => {
737 *postgis_types.entry("geometry".to_string()).or_insert(0) += 1;
738 }
739 PostgreSQLDataType::Geography(_) => {
740 *postgis_types.entry("geography".to_string()).or_insert(0) += 1;
741 }
742 PostgreSQLDataType::Array(inner) => match inner.as_ref() {
743 PostgreSQLDataType::Geometry(_) => {
744 *postgis_types.entry("geometry[]".to_string()).or_insert(0) += 1;
745 }
746 PostgreSQLDataType::Geography(_) => {
747 *postgis_types.entry("geography[]".to_string()).or_insert(0) += 1;
748 }
749 _ => {}
750 },
751 _ => {}
752 }
753 }
754
755 postgis_types
756 }
757
758 pub fn to_ddl(&self) -> String {
760 let mut ddl = format!(
761 "CREATE TABLE {}.{} (\n",
762 escape_db_identifier(&self.table_schema),
763 escape_db_identifier(&self.table_name)
764 );
765
766 let column_definitions: Vec<String> = self
768 .columns
769 .iter()
770 .map(|col| format!(" {}", col.to_ddl()))
771 .collect();
772
773 let mut constraints = Vec::new();
774
775 let primary_key_columns: Vec<&str> = self
777 .columns
778 .iter()
779 .filter(|col| col.is_primary_key)
780 .map(|col| col.column_name.as_str())
781 .collect();
782
783 if !primary_key_columns.is_empty() {
784 constraints.push(format!(
785 " CONSTRAINT pk_{} PRIMARY KEY ({})",
786 self.table_name,
787 primary_key_columns
788 .iter()
789 .map(|v| escape_db_identifier(v))
790 .collect::<Vec<_>>()
791 .join(", ")
792 ));
793 }
794
795 for fk in &self.foreign_keys {
797 let delete_action = match &fk.on_delete {
798 Some(action) => format!(" ON DELETE {}", action),
799 None => String::new(),
800 };
801 let update_action = match &fk.on_update {
802 Some(action) => format!(" ON UPDATE {}", action),
803 None => String::new(),
804 };
805 let deferrable = if fk.is_deferrable {
806 if fk.initially_deferred {
807 " DEFERRABLE INITIALLY DEFERRED"
808 } else {
809 " DEFERRABLE INITIALLY IMMEDIATE"
810 }
811 } else {
812 ""
813 };
814
815 constraints.push(format!(
816 " CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){}{}{}",
817 escape_db_identifier(&fk.constraint_name),
818 fk.columns
819 .iter()
820 .map(|v| escape_db_identifier(v))
821 .collect::<Vec<_>>()
822 .join(", "),
823 escape_db_identifier(&fk.referenced_table),
824 fk.referenced_columns
825 .iter()
826 .map(|v| escape_db_identifier(v))
827 .collect::<Vec<_>>()
828 .join(", "),
829 delete_action,
830 update_action,
831 deferrable
832 ));
833 }
834
835 for check in &self.check_constraints {
837 constraints.push(format!(
838 " CONSTRAINT {} CHECK ({})",
839 escape_db_identifier(&check.constraint_name),
840 check.check_clause
841 ));
842 }
843
844 let mut all_definitions = column_definitions;
846 all_definitions.extend(constraints);
847
848 ddl.push_str(&all_definitions.join(",\n"));
849 ddl.push_str("\n);");
850
851 ddl
852 }
853
854 pub fn to_index_ddl(&self) -> Vec<String> {
856 self.indexes
857 .iter()
858 .filter(|idx| !idx.is_primary) .map(|idx| idx.to_ddl(&self.table_schema, &self.table_name))
860 .collect()
861 }
862
863 pub fn to_comment_ddl(&self) -> Vec<String> {
865 let mut comments = Vec::new();
866
867 if let Some(table_comment) = &self.comment {
869 comments.push(format!(
870 "COMMENT ON TABLE {}.{} IS '{}'",
871 escape_db_identifier(&self.table_schema),
872 escape_db_identifier(&self.table_name),
873 table_comment.replace("'", "''") ));
875 }
876
877 for col in &self.columns {
879 if let Some(comment) = &col.comment {
880 comments.push(format!(
881 "COMMENT ON COLUMN {}.{}.{} IS '{}'",
882 escape_db_identifier(&self.table_schema),
883 escape_db_identifier(&self.table_name),
884 escape_db_identifier(&col.column_name),
885 comment.replace("'", "''") ));
887 }
888 }
889
890 comments
891 }
892
893 pub fn get_referenced_tables(&self) -> Vec<String> {
895 self.foreign_keys
896 .iter()
897 .map(|fk| fk.referenced_table.clone())
898 .collect()
899 }
900
901 pub fn has_foreign_keys(&self) -> bool {
903 !self.foreign_keys.is_empty()
904 }
905}
906
907#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
909pub struct DatabaseSchema {
910 pub tables: Vec<TableInfo>,
911}
912
913impl DatabaseSchema {
914 pub fn requires_postgis(&self) -> bool {
916 self.tables.iter().any(|table| table.uses_postgis())
917 }
918
919 pub fn get_postgis_tables(&self) -> Vec<&TableInfo> {
921 self.tables
922 .iter()
923 .filter(|table| table.uses_postgis())
924 .collect()
925 }
926
927 pub fn get_all_postgis_types(&self) -> HashMap<String, i32> {
929 let mut all_types = HashMap::new();
930
931 for table in &self.tables {
932 let table_types = table.get_postgis_types();
933 for (type_name, count) in table_types {
934 *all_types.entry(type_name).or_insert(0) += count;
935 }
936 }
937
938 all_types
939 }
940
941 pub fn to_create_ddl(&self) -> String {
943 let mut ddl_parts = Vec::new();
944
945 if self.requires_postgis() {
947 ddl_parts.push("CREATE EXTENSION IF NOT EXISTS postgis;".to_string());
948 ddl_parts.push("".to_string()); }
950
951 let mut created_tables = std::collections::HashSet::new();
953 let mut remaining_tables: Vec<&TableInfo> = self.tables.iter().collect();
954
955 while !remaining_tables.is_empty() {
957 let mut progress = false;
958
959 remaining_tables.retain(|table| {
960 let can_create = !table.has_foreign_keys()
961 || table.get_referenced_tables().iter().all(|ref_table| {
962 created_tables.contains(ref_table) || ref_table == &table.table_name
963 });
964
965 if can_create {
966 ddl_parts.push(table.to_ddl());
967 ddl_parts.push("".to_string()); let comment_ddls = table.to_comment_ddl();
971 for comment_ddl in comment_ddls {
972 ddl_parts.push(comment_ddl);
973 }
974 if !table.to_comment_ddl().is_empty() {
975 ddl_parts.push("".to_string()); }
977
978 let index_ddls = table.to_index_ddl();
980 for index_ddl in index_ddls {
981 ddl_parts.push(index_ddl);
982 }
983 if !table.to_index_ddl().is_empty() {
984 ddl_parts.push("".to_string()); }
986
987 created_tables.insert(table.table_name.clone());
988 progress = true;
989 false } else {
991 true }
993 });
994
995 if !progress && !remaining_tables.is_empty() {
997 let table = remaining_tables.remove(0);
998 ddl_parts.push(table.to_ddl());
999 ddl_parts.push("".to_string());
1000
1001 let comment_ddls = table.to_comment_ddl();
1003 for comment_ddl in comment_ddls {
1004 ddl_parts.push(comment_ddl);
1005 }
1006 if !table.to_comment_ddl().is_empty() {
1007 ddl_parts.push("".to_string()); }
1009
1010 let index_ddls = table.to_index_ddl();
1012 for index_ddl in index_ddls {
1013 ddl_parts.push(index_ddl);
1014 }
1015 if !table.to_index_ddl().is_empty() {
1016 ddl_parts.push("".to_string()); }
1018
1019 created_tables.insert(table.table_name.clone());
1020 }
1021 }
1022
1023 ddl_parts.join("\n")
1024 }
1025
1026 pub fn to_drop_ddl(&self) -> String {
1028 let mut ddl_parts = Vec::new();
1029
1030 let mut dropped_tables = std::collections::HashSet::new();
1032 let mut remaining_tables: Vec<&TableInfo> = self.tables.iter().collect();
1033
1034 while !remaining_tables.is_empty() {
1035 let mut progress = false;
1036
1037 remaining_tables.retain(|table| {
1038 let can_drop = self.tables.iter().all(|other_table| {
1039 other_table.table_name == table.table_name
1040 || dropped_tables.contains(&other_table.table_name)
1041 || !other_table
1042 .get_referenced_tables()
1043 .contains(&table.table_name)
1044 });
1045
1046 if can_drop {
1047 ddl_parts.push(format!(
1048 "DROP TABLE IF EXISTS {}.{} CASCADE;",
1049 escape_db_identifier(&table.table_schema),
1050 escape_db_identifier(&table.table_name)
1051 ));
1052 dropped_tables.insert(table.table_name.clone());
1053 progress = true;
1054 false
1055 } else {
1056 true
1057 }
1058 });
1059
1060 if !progress && !remaining_tables.is_empty() {
1061 let table = remaining_tables.remove(0);
1062 ddl_parts.push(format!(
1063 "DROP TABLE IF EXISTS {}.{} CASCADE;",
1064 escape_db_identifier(&table.table_schema),
1065 escape_db_identifier(&table.table_name)
1066 ));
1067 dropped_tables.insert(table.table_name.clone());
1068 }
1069 }
1070
1071 ddl_parts.join("\n")
1072 }
1073}
1074
1075pub async fn connect_to_database(database_url: &str) -> Result<PgPool, sqlx::Error> {
1077 PgPool::connect(database_url).await
1078}
1079
1080pub async fn get_current_schema(pool: &PgPool) -> Result<String, sqlx::Error> {
1082 let row = sqlx::query("SELECT current_schema()")
1083 .fetch_one(pool)
1084 .await?;
1085 Ok(row.get(0))
1086}
1087
1088pub async fn get_database_schema(pool: &PgPool) -> Result<DatabaseSchema, sqlx::Error> {
1090 let current_schema = get_current_schema(pool).await?;
1091 let table_names = get_table_names(pool, ¤t_schema).await?;
1092
1093 let mut tables = Vec::new();
1094 for table_name in table_names {
1095 let columns = get_table_columns(pool, ¤t_schema, &table_name).await?;
1096 let foreign_keys = get_foreign_key_constraints(pool, ¤t_schema, &table_name).await?;
1097 let indexes = get_table_indexes(pool, ¤t_schema, &table_name).await?;
1098 let check_constraints = get_check_constraints(pool, ¤t_schema, &table_name).await?;
1099 let table_comment = get_table_comment(pool, ¤t_schema, &table_name).await?;
1100
1101 tables.push(TableInfo {
1102 table_schema: current_schema.clone(),
1103 table_name,
1104 columns,
1105 foreign_keys,
1106 indexes,
1107 check_constraints,
1108 comment: table_comment,
1109 });
1110 }
1111
1112 Ok(DatabaseSchema { tables })
1113}
1114
1115pub async fn get_table_names(pool: &PgPool, schema: &str) -> Result<Vec<String>, sqlx::Error> {
1118 let rows = sqlx::query(
1119 "SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE' ORDER BY table_name"
1120 )
1121 .bind(schema)
1122 .fetch_all(pool)
1123 .await?;
1124
1125 Ok(rows.into_iter().map(|row| row.get(0)).collect())
1126}
1127
1128pub async fn get_table_columns(
1129 pool: &PgPool,
1130 schema: &str,
1131 table: &str,
1132) -> Result<Vec<ColumnInfo>, sqlx::Error> {
1133 let query = r#"
1134 SELECT
1135 c.column_name,
1136 c.data_type,
1137 c.is_nullable,
1138 c.column_default,
1139 c.character_maximum_length,
1140 c.numeric_precision,
1141 c.numeric_scale,
1142 c.udt_name,
1143 COALESCE(pk.is_primary, false) as is_primary_key,
1144 pgd.description as comment,
1145 c.collation_name
1146 FROM information_schema.columns c
1147 LEFT JOIN (
1148 SELECT
1149 kcu.column_name,
1150 true as is_primary
1151 FROM information_schema.table_constraints tc
1152 JOIN information_schema.key_column_usage kcu
1153 ON tc.constraint_name = kcu.constraint_name
1154 AND tc.table_schema = kcu.table_schema
1155 WHERE tc.constraint_type = 'PRIMARY KEY'
1156 AND tc.table_schema = $1
1157 AND tc.table_name = $2
1158 ) pk ON c.column_name = pk.column_name
1159 LEFT JOIN pg_class pgc ON pgc.relname = c.table_name AND pgc.relnamespace = (
1160 SELECT oid FROM pg_namespace WHERE nspname = c.table_schema
1161 )
1162 LEFT JOIN pg_attribute pga ON pga.attrelid = pgc.oid AND pga.attname = c.column_name
1163 LEFT JOIN pg_description pgd ON pgd.objoid = pgc.oid AND pgd.objsubid = pga.attnum
1164 WHERE c.table_schema = $1 AND c.table_name = $2
1165 ORDER BY c.ordinal_position
1166 "#;
1167
1168 let rows = sqlx::query(query)
1169 .bind(schema)
1170 .bind(table)
1171 .fetch_all(pool)
1172 .await?;
1173
1174 let mut columns = Vec::new();
1175 for row in rows {
1176 let column_name: String = row.get("column_name");
1177 let data_type_str: String = row.get("data_type");
1178 let udt_name: String = row.get("udt_name");
1179 let is_nullable_str: String = row.get("is_nullable");
1180 let is_nullable = is_nullable_str == "YES";
1181 let column_default_str: Option<String> = row.get("column_default");
1182 let character_maximum_length: Option<i32> = row.get("character_maximum_length");
1183 let numeric_precision: Option<i32> = row.get("numeric_precision");
1184 let numeric_scale: Option<i32> = row.get("numeric_scale");
1185 let is_primary_key: bool = row.get("is_primary_key");
1186 let comment: Option<String> = row.get("comment");
1187 let collation_name: Option<String> = row.get("collation_name");
1188
1189 let column_default = column_default_str.as_ref().map(|s| parse_default_value(s));
1191
1192 let data_type = parse_data_type(
1193 &data_type_str,
1194 &udt_name,
1195 character_maximum_length,
1196 numeric_precision,
1197 numeric_scale,
1198 &column_default_str,
1199 &column_name,
1200 table,
1201 pool,
1202 schema,
1203 )
1204 .await;
1205
1206 columns.push(ColumnInfo {
1207 column_name,
1208 data_type,
1209 is_nullable,
1210 column_default,
1211 character_maximum_length,
1212 numeric_precision,
1213 numeric_scale,
1214 is_primary_key,
1215 comment,
1216 collate: collation_name,
1217 });
1218 }
1219
1220 Ok(columns)
1221}
1222
1223async fn parse_data_type(
1224 data_type_str: &str,
1225 udt_name: &str,
1226 character_maximum_length: Option<i32>,
1227 numeric_precision: Option<i32>,
1228 numeric_scale: Option<i32>,
1229 column_default: &Option<String>,
1230 column_name: &str,
1231 table_name: &str,
1232 pool: &PgPool,
1233 schema: &str,
1234) -> PostgreSQLDataType {
1235 let is_serial = if let Some(default_value) = column_default {
1237 default_value.contains("nextval")
1238 && (default_value.contains(&format!("{}_seq", column_name))
1239 || default_value.contains(&format!("{}_{}_seq", table_name, column_name)))
1240 } else {
1241 false
1242 };
1243
1244 match data_type_str {
1245 "smallint" => {
1246 if is_serial {
1247 PostgreSQLDataType::SmallSerial
1248 } else {
1249 PostgreSQLDataType::SmallInt
1250 }
1251 }
1252 "integer" => {
1253 if is_serial {
1254 PostgreSQLDataType::Serial
1255 } else {
1256 PostgreSQLDataType::Integer
1257 }
1258 }
1259 "bigint" => {
1260 if is_serial {
1261 PostgreSQLDataType::BigSerial
1262 } else {
1263 PostgreSQLDataType::BigInt
1264 }
1265 }
1266 "real" => PostgreSQLDataType::Real,
1267 "double precision" => PostgreSQLDataType::DoublePrecision,
1268 "numeric" => PostgreSQLDataType::Numeric(numeric_precision, numeric_scale),
1269 "character varying" => PostgreSQLDataType::Varchar(character_maximum_length),
1270 "character" => PostgreSQLDataType::Char(character_maximum_length),
1271 "text" => PostgreSQLDataType::Text,
1272 "bytea" => PostgreSQLDataType::Bytea,
1273 "date" => PostgreSQLDataType::Date,
1274 "time without time zone" => PostgreSQLDataType::Time,
1275 "time with time zone" => PostgreSQLDataType::TimeWithTimeZone,
1276 "timestamp without time zone" => PostgreSQLDataType::Timestamp,
1277 "timestamp with time zone" => PostgreSQLDataType::TimestampWithTimeZone,
1278 "interval" => PostgreSQLDataType::Interval,
1279 "boolean" => PostgreSQLDataType::Boolean,
1280 "uuid" => PostgreSQLDataType::Uuid,
1281 "json" => PostgreSQLDataType::Json,
1282 "jsonb" => PostgreSQLDataType::Jsonb,
1283 "ARRAY" => {
1284 let base_type = parse_base_array_type(
1285 udt_name,
1286 column_default,
1287 column_name,
1288 table_name,
1289 pool,
1290 schema,
1291 )
1292 .await;
1293 PostgreSQLDataType::Array(Box::new(base_type))
1294 }
1295 "USER-DEFINED" => match udt_name {
1296 "geometry" => {
1297 let geo_info =
1298 get_geometry_info(pool, schema, table_name, column_name, "geometry_columns")
1299 .await;
1300 PostgreSQLDataType::Geometry(geo_info)
1301 }
1302 "geography" => {
1303 let geo_info =
1304 get_geometry_info(pool, schema, table_name, column_name, "geography_columns")
1305 .await;
1306 PostgreSQLDataType::Geography(geo_info)
1307 }
1308 _ => {
1309 if udt_name.starts_with("_") {
1310 let base_udt = &udt_name[1..];
1311 let base_type = match base_udt {
1312 "geometry" => PostgreSQLDataType::Geometry(None),
1313 "geography" => PostgreSQLDataType::Geography(None),
1314 _ => PostgreSQLDataType::Custom(base_udt.to_string()),
1315 };
1316 PostgreSQLDataType::Array(Box::new(base_type))
1317 } else {
1318 PostgreSQLDataType::Custom(udt_name.to_string())
1319 }
1320 }
1321 },
1322 _ => PostgreSQLDataType::Unknown(data_type_str.to_string()),
1323 }
1324}
1325
1326async fn parse_base_array_type(
1327 udt_name: &str,
1328 _column_default: &Option<String>,
1329 _column_name: &str,
1330 _table_name: &str,
1331 _pool: &PgPool,
1332 _schema: &str,
1333) -> PostgreSQLDataType {
1334 if udt_name.starts_with("_") {
1335 let base_type = &udt_name[1..];
1336 match base_type {
1337 "int2" => PostgreSQLDataType::SmallInt,
1338 "int4" => PostgreSQLDataType::Integer,
1339 "int8" => PostgreSQLDataType::BigInt,
1340 "float4" => PostgreSQLDataType::Real,
1341 "float8" => PostgreSQLDataType::DoublePrecision,
1342 "numeric" => PostgreSQLDataType::Numeric(None, None),
1343 "varchar" => PostgreSQLDataType::Varchar(None),
1344 "char" => PostgreSQLDataType::Char(None),
1345 "text" => PostgreSQLDataType::Text,
1346 "bytea" => PostgreSQLDataType::Bytea,
1347 "bool" => PostgreSQLDataType::Boolean,
1348 "uuid" => PostgreSQLDataType::Uuid,
1349 "json" => PostgreSQLDataType::Json,
1350 "jsonb" => PostgreSQLDataType::Jsonb,
1351 "geometry" => PostgreSQLDataType::Geometry(None),
1352 "geography" => PostgreSQLDataType::Geography(None),
1353 _ => PostgreSQLDataType::Custom(base_type.to_string()),
1354 }
1355 } else {
1356 PostgreSQLDataType::Unknown(udt_name.to_string())
1357 }
1358}
1359
1360async fn get_geometry_info(
1361 pool: &PgPool,
1362 schema: &str,
1363 table_name: &str,
1364 column_name: &str,
1365 metadata_table: &str,
1366) -> Option<GeometryInfo> {
1367 let column_field = if metadata_table == "geography_columns" {
1369 "f_geography_column"
1370 } else {
1371 "f_geometry_column"
1372 };
1373
1374 let query = format!(
1375 "SELECT type, srid, coord_dimension FROM {} WHERE f_table_schema = $1 AND f_table_name = $2 AND {} = $3",
1376 metadata_table, column_field
1377 );
1378
1379 if let Ok(row) = sqlx::query(&query)
1380 .bind(schema)
1381 .bind(table_name)
1382 .bind(column_name)
1383 .fetch_optional(pool)
1384 .await
1385 {
1386 if let Some(row) = row {
1387 let geometry_type_str: String = row.get("type");
1388 let srid: Option<i32> = row.get("srid");
1389 let dimension: Option<i32> = row.get("coord_dimension");
1390
1391 let geometry_type = match geometry_type_str.to_uppercase().as_str() {
1392 "POINT" => GeometryType::Point,
1393 "LINESTRING" => GeometryType::LineString,
1394 "POLYGON" => GeometryType::Polygon,
1395 "MULTIPOINT" => GeometryType::MultiPoint,
1396 "MULTILINESTRING" => GeometryType::MultiLineString,
1397 "MULTIPOLYGON" => GeometryType::MultiPolygon,
1398 "GEOMETRYCOLLECTION" => GeometryType::GeometryCollection,
1399 _ => return None,
1400 };
1401
1402 return Some(GeometryInfo {
1403 geometry_type,
1404 srid,
1405 dimension,
1406 });
1407 }
1408 }
1409
1410 None
1411}
1412
1413pub async fn get_foreign_key_constraints(
1414 pool: &PgPool,
1415 schema: &str,
1416 table: &str,
1417) -> Result<Vec<ForeignKeyConstraint>, sqlx::Error> {
1418 let query = r#"
1419 SELECT
1420 con.conname as constraint_name,
1421 array_agg(att.attname ORDER BY u.attposition)::text as columns,
1422 ref_class.relname as referenced_table,
1423 array_agg(ref_att.attname ORDER BY u.attposition)::text as referenced_columns,
1424 CASE con.confdeltype
1425 WHEN 'a' THEN NULL
1426 WHEN 'r' THEN 'RESTRICT'
1427 WHEN 'c' THEN 'CASCADE'
1428 WHEN 'n' THEN 'SET NULL'
1429 WHEN 'd' THEN 'SET DEFAULT'
1430 END as on_delete,
1431 CASE con.confupdtype
1432 WHEN 'a' THEN NULL
1433 WHEN 'r' THEN 'RESTRICT'
1434 WHEN 'c' THEN 'CASCADE'
1435 WHEN 'n' THEN 'SET NULL'
1436 WHEN 'd' THEN 'SET DEFAULT'
1437 END as on_update,
1438 con.condeferrable as is_deferrable,
1439 con.condeferred as initially_deferred
1440 FROM pg_constraint con
1441 JOIN pg_class class ON con.conrelid = class.oid
1442 JOIN pg_namespace nsp ON class.relnamespace = nsp.oid
1443 JOIN pg_class ref_class ON con.confrelid = ref_class.oid
1444 CROSS JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS u(attnum, attposition)
1445 JOIN pg_attribute att ON att.attrelid = class.oid AND att.attnum = u.attnum
1446 CROSS JOIN LATERAL unnest(con.confkey) WITH ORDINALITY AS u_ref(ref_attnum, ref_attposition)
1447 JOIN pg_attribute ref_att ON ref_att.attrelid = ref_class.oid
1448 AND ref_att.attnum = u_ref.ref_attnum
1449 AND u.attposition = u_ref.ref_attposition
1450 WHERE nsp.nspname = $1
1451 AND class.relname = $2
1452 AND con.contype = 'f'
1453 GROUP BY con.conname, ref_class.relname, con.confdeltype, con.confupdtype,
1454 con.condeferrable, con.condeferred
1455 ORDER BY con.conname
1456 "#;
1457
1458 let rows = sqlx::query(query)
1459 .bind(schema)
1460 .bind(table)
1461 .fetch_all(pool)
1462 .await?;
1463
1464 let mut constraints = Vec::new();
1465 for row in rows {
1466 let constraint_name: String = row.get("constraint_name");
1467 let columns_str: String = row.get("columns");
1468 let columns = parse_postgres_array(&columns_str);
1469
1470 let referenced_table: String = row.get("referenced_table");
1471
1472 let referenced_columns_str: String = row.get("referenced_columns");
1473 let referenced_columns = parse_postgres_array(&referenced_columns_str);
1474 let on_delete: Option<String> = row.get("on_delete");
1475 let on_update: Option<String> = row.get("on_update");
1476 let is_deferrable: bool = row.get("is_deferrable");
1477 let initially_deferred: bool = row.get("initially_deferred");
1478
1479 let on_delete_action = on_delete.as_deref().and_then(|action| {
1480 if action == "NO ACTION" {
1481 None
1482 } else {
1483 ForeignKeyAction::from_str(action)
1484 }
1485 });
1486
1487 let on_update_action = on_update.as_deref().and_then(|action| {
1488 if action == "NO ACTION" {
1489 None
1490 } else {
1491 ForeignKeyAction::from_str(action)
1492 }
1493 });
1494
1495 constraints.push(ForeignKeyConstraint {
1496 constraint_name,
1497 columns,
1498 referenced_table,
1499 referenced_columns,
1500 on_delete: on_delete_action,
1501 on_update: on_update_action,
1502 is_deferrable,
1503 initially_deferred,
1504 });
1505 }
1506
1507 Ok(constraints)
1508}
1509
1510pub async fn get_table_indexes(
1511 pool: &PgPool,
1512 schema: &str,
1513 table: &str,
1514) -> Result<Vec<IndexInfo>, sqlx::Error> {
1515 let query = r#"
1516 SELECT
1517 i.relname as index_name,
1518 array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum))::text as columns,
1519 ix.indisunique as is_unique,
1520 ix.indisprimary as is_primary,
1521 am.amname as index_type,
1522 pg_get_expr(ix.indpred, ix.indrelid) as condition
1523 FROM pg_index ix
1524 JOIN pg_class i ON i.oid = ix.indexrelid
1525 JOIN pg_class t ON t.oid = ix.indrelid
1526 JOIN pg_namespace n ON n.oid = t.relnamespace
1527 JOIN pg_am am ON am.oid = i.relam
1528 JOIN pg_attribute a ON a.attrelid = ix.indrelid AND a.attnum = ANY(ix.indkey)
1529 WHERE n.nspname = $1
1530 AND t.relname = $2
1531 GROUP BY i.relname, ix.indisunique, ix.indisprimary, am.amname, ix.indpred, ix.indrelid
1532 ORDER BY i.relname
1533 "#;
1534
1535 let rows = sqlx::query(query)
1536 .bind(schema)
1537 .bind(table)
1538 .fetch_all(pool)
1539 .await?;
1540
1541 let mut indexes = Vec::new();
1542 for row in rows {
1543 let index_name: String = row.get("index_name");
1544 let columns_str: String = row.get("columns");
1545 let columns = parse_postgres_array(&columns_str);
1546 let is_unique: bool = row.get("is_unique");
1547 let is_primary: bool = row.get("is_primary");
1548 let index_type_str: String = row.get("index_type");
1549 let index_type = IndexType::from_str(&index_type_str).unwrap_or(IndexType::BTree);
1550 let condition: Option<String> = row.get("condition");
1551
1552 indexes.push(IndexInfo {
1553 index_name,
1554 columns,
1555 is_unique,
1556 is_primary,
1557 index_type,
1558 condition,
1559 });
1560 }
1561
1562 Ok(indexes)
1563}
1564
1565pub async fn get_check_constraints(
1566 pool: &PgPool,
1567 schema: &str,
1568 table: &str,
1569) -> Result<Vec<CheckConstraint>, sqlx::Error> {
1570 let query = r#"
1571 SELECT
1572 cc.constraint_name,
1573 cc.check_clause
1574 FROM information_schema.check_constraints cc
1575 JOIN information_schema.table_constraints tc
1576 ON cc.constraint_name = tc.constraint_name
1577 AND cc.constraint_schema = tc.constraint_schema
1578 WHERE tc.table_schema = $1
1579 AND tc.table_name = $2
1580 AND tc.constraint_type = 'CHECK'
1581 ORDER BY cc.constraint_name
1582 "#;
1583
1584 let rows = sqlx::query(query)
1585 .bind(schema)
1586 .bind(table)
1587 .fetch_all(pool)
1588 .await?;
1589
1590 let mut constraints = Vec::new();
1591 for row in rows {
1592 let constraint_name: String = row.get("constraint_name");
1593 let check_clause: String = row.get("check_clause");
1594
1595 if is_not_null_constraint(&constraint_name, &check_clause) {
1597 continue;
1598 }
1599
1600 constraints.push(CheckConstraint {
1601 constraint_name,
1602 check_clause,
1603 });
1604 }
1605
1606 Ok(constraints)
1607}
1608
1609pub async fn get_table_comment(
1610 pool: &PgPool,
1611 schema: &str,
1612 table: &str,
1613) -> Result<Option<String>, sqlx::Error> {
1614 let query = r#"
1615 SELECT
1616 pgd.description
1617 FROM pg_class pgc
1618 JOIN pg_namespace pgn ON pgn.oid = pgc.relnamespace
1619 LEFT JOIN pg_description pgd ON pgd.objoid = pgc.oid AND pgd.objsubid = 0
1620 WHERE pgn.nspname = $1 AND pgc.relname = $2
1621 "#;
1622
1623 let row = sqlx::query(query)
1624 .bind(schema)
1625 .bind(table)
1626 .fetch_optional(pool)
1627 .await?;
1628
1629 Ok(row.and_then(|r| r.get("description")))
1630}
1631
1632fn is_not_null_constraint(constraint_name: &str, check_clause: &str) -> bool {
1634 if constraint_name.ends_with("_not_null") {
1639 let parts: Vec<&str> = constraint_name.split('_').collect();
1641
1642 if parts.len() == 5 && parts[3] == "not" && parts[4] == "null" {
1644 if parts[0].chars().all(|c| c.is_ascii_digit())
1646 && parts[1].chars().all(|c| c.is_ascii_digit())
1647 && parts[2].chars().all(|c| c.is_ascii_digit())
1648 {
1649 return check_clause.contains("IS NOT NULL");
1651 }
1652 }
1653 }
1654
1655 false
1656}
1657
1658pub fn is_system_generated_index(idx: &IndexInfo) -> bool {
1660 let name = &idx.index_name;
1666
1667 if idx.is_primary {
1669 return true;
1670 }
1671
1672 if name.ends_with("_pkey")
1674 || name.contains("_not_null")
1675 || name.chars().next().map_or(false, |c| c.is_ascii_digit())
1676 {
1677 return true;
1678 }
1679
1680 false
1682}
1683
1684pub fn convert_mysql_placeholders_to_postgresql(sql: &str) -> String {
1726 let mut result = String::with_capacity(sql.len() + 32); let mut chars = sql.chars().peekable();
1728 let mut placeholder_count = 0;
1729 let mut in_single_quote = false;
1730 let mut in_double_quote = false;
1731 let mut in_line_comment = false;
1732 let mut in_block_comment = false;
1733
1734 while let Some(ch) = chars.next() {
1735 match ch {
1736 '\'' if !in_double_quote && !in_line_comment && !in_block_comment => {
1738 in_single_quote = !in_single_quote;
1739 result.push(ch);
1740 }
1741 '"' if !in_single_quote && !in_line_comment && !in_block_comment => {
1742 in_double_quote = !in_double_quote;
1743 result.push(ch);
1744 }
1745
1746 '-' if !in_single_quote && !in_double_quote && !in_block_comment => {
1748 if chars.peek() == Some(&'-') {
1749 chars.next(); in_line_comment = true;
1751 result.push_str("--");
1752 } else {
1753 result.push(ch);
1754 }
1755 }
1756 '/' if !in_single_quote && !in_double_quote && !in_line_comment => {
1757 if chars.peek() == Some(&'*') {
1758 chars.next(); in_block_comment = true;
1760 result.push_str("/*");
1761 } else {
1762 result.push(ch);
1763 }
1764 }
1765 '*' if in_block_comment => {
1766 if chars.peek() == Some(&'/') {
1767 chars.next(); in_block_comment = false;
1769 result.push_str("*/");
1770 } else {
1771 result.push(ch);
1772 }
1773 }
1774 '\n' | '\r' if in_line_comment => {
1775 in_line_comment = false;
1776 result.push(ch);
1777 }
1778
1779 '\\' if (in_single_quote || in_double_quote)
1781 && !in_line_comment
1782 && !in_block_comment =>
1783 {
1784 result.push(ch);
1785 if let Some(next_ch) = chars.next() {
1786 result.push(next_ch); }
1788 }
1789
1790 '\\' if !in_single_quote
1792 && !in_double_quote
1793 && !in_line_comment
1794 && !in_block_comment =>
1795 {
1796 if chars.peek() == Some(&'?') {
1797 chars.next(); result.push('?'); } else {
1800 result.push(ch);
1801 }
1802 }
1803
1804 '?' if !in_single_quote
1806 && !in_double_quote
1807 && !in_line_comment
1808 && !in_block_comment =>
1809 {
1810 if chars.peek() == Some(&'?') {
1812 chars.next(); result.push('?'); } else {
1815 placeholder_count += 1;
1817 result.push('$');
1818 result.push_str(&placeholder_count.to_string());
1819 }
1820 }
1821
1822 _ => {
1824 result.push(ch);
1825 }
1826 }
1827 }
1828
1829 result
1830}
1831
1832pub fn escape_db_identifier(s: &str) -> String {
1833 format!(r#""{}""#, s.replace('"', r#""""#))
1834}