1use serde::{Deserialize, Serialize};
21
22use crate::sql::DatabaseType;
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct DatabaseSchema {
31 pub name: String,
33 pub schema: Option<String>,
35 pub tables: Vec<TableInfo>,
37 pub views: Vec<ViewInfo>,
39 pub enums: Vec<EnumInfo>,
41 pub sequences: Vec<SequenceInfo>,
43}
44
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct TableInfo {
48 pub name: String,
50 pub schema: Option<String>,
52 pub comment: Option<String>,
54 pub columns: Vec<ColumnInfo>,
56 pub primary_key: Vec<String>,
58 pub foreign_keys: Vec<ForeignKeyInfo>,
60 pub indexes: Vec<IndexInfo>,
62 pub unique_constraints: Vec<UniqueConstraint>,
64 pub check_constraints: Vec<CheckConstraint>,
66}
67
68#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70pub struct ColumnInfo {
71 pub name: String,
73 pub db_type: String,
75 pub normalized_type: NormalizedType,
77 pub nullable: bool,
79 pub default: Option<String>,
81 pub auto_increment: bool,
83 pub is_primary_key: bool,
85 pub is_unique: bool,
87 pub comment: Option<String>,
89 pub max_length: Option<i32>,
91 pub precision: Option<i32>,
93 pub scale: Option<i32>,
95 pub enum_name: Option<String>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub enum NormalizedType {
102 Int,
104 BigInt,
105 SmallInt,
106 Float,
108 Double,
109 Decimal {
111 precision: Option<i32>,
112 scale: Option<i32>,
113 },
114 String,
116 Text,
117 Char {
118 length: Option<i32>,
119 },
120 VarChar {
121 length: Option<i32>,
122 },
123 Bytes,
125 Boolean,
127 DateTime,
129 Date,
130 Time,
131 Timestamp,
132 Json,
134 Uuid,
136 Array(Box<NormalizedType>),
138 Enum(String),
140 Unknown(String),
142}
143
144impl Default for NormalizedType {
145 fn default() -> Self {
146 Self::Unknown("unknown".to_string())
147 }
148}
149
150impl NormalizedType {
151 pub fn to_prax_type(&self) -> String {
153 match self {
154 Self::Int => "Int".to_string(),
155 Self::BigInt => "BigInt".to_string(),
156 Self::SmallInt => "Int".to_string(),
157 Self::Float => "Float".to_string(),
158 Self::Double => "Float".to_string(),
159 Self::Decimal { .. } => "Decimal".to_string(),
160 Self::String | Self::Text | Self::VarChar { .. } | Self::Char { .. } => {
161 "String".to_string()
162 }
163 Self::Bytes => "Bytes".to_string(),
164 Self::Boolean => "Boolean".to_string(),
165 Self::DateTime | Self::Timestamp => "DateTime".to_string(),
166 Self::Date => "DateTime".to_string(),
167 Self::Time => "DateTime".to_string(),
168 Self::Json => "Json".to_string(),
169 Self::Uuid => "String".to_string(), Self::Array(inner) => format!("{}[]", inner.to_prax_type()),
171 Self::Enum(name) => name.clone(),
172 Self::Unknown(t) => format!("Unsupported<{}>", t),
173 }
174 }
175}
176
177#[derive(Debug, Clone, Default, Serialize, Deserialize)]
179pub struct ForeignKeyInfo {
180 pub name: String,
182 pub columns: Vec<String>,
184 pub referenced_table: String,
186 pub referenced_schema: Option<String>,
188 pub referenced_columns: Vec<String>,
190 pub on_delete: ReferentialAction,
192 pub on_update: ReferentialAction,
194}
195
196#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
198pub enum ReferentialAction {
199 #[default]
200 NoAction,
201 Restrict,
202 Cascade,
203 SetNull,
204 SetDefault,
205}
206
207impl ReferentialAction {
208 pub fn to_prax(&self) -> &'static str {
210 match self {
211 Self::NoAction => "NoAction",
212 Self::Restrict => "Restrict",
213 Self::Cascade => "Cascade",
214 Self::SetNull => "SetNull",
215 Self::SetDefault => "SetDefault",
216 }
217 }
218
219 pub fn from_str(s: &str) -> Self {
221 match s.to_uppercase().as_str() {
222 "NO ACTION" | "NOACTION" => Self::NoAction,
223 "RESTRICT" => Self::Restrict,
224 "CASCADE" => Self::Cascade,
225 "SET NULL" | "SETNULL" => Self::SetNull,
226 "SET DEFAULT" | "SETDEFAULT" => Self::SetDefault,
227 _ => Self::NoAction,
228 }
229 }
230}
231
232#[derive(Debug, Clone, Default, Serialize, Deserialize)]
234pub struct IndexInfo {
235 pub name: String,
237 pub columns: Vec<IndexColumn>,
239 pub is_unique: bool,
241 pub is_primary: bool,
243 pub index_type: Option<String>,
245 pub filter: Option<String>,
247}
248
249#[derive(Debug, Clone, Default, Serialize, Deserialize)]
251pub struct IndexColumn {
252 pub name: String,
254 pub order: SortOrder,
256 pub nulls: NullsOrder,
258}
259
260#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
262pub enum SortOrder {
263 #[default]
264 Asc,
265 Desc,
266}
267
268#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
270pub enum NullsOrder {
271 #[default]
272 Last,
273 First,
274}
275
276#[derive(Debug, Clone, Default, Serialize, Deserialize)]
278pub struct UniqueConstraint {
279 pub name: String,
281 pub columns: Vec<String>,
283}
284
285#[derive(Debug, Clone, Default, Serialize, Deserialize)]
287pub struct CheckConstraint {
288 pub name: String,
290 pub expression: String,
292}
293
294#[derive(Debug, Clone, Default, Serialize, Deserialize)]
296pub struct ViewInfo {
297 pub name: String,
299 pub schema: Option<String>,
301 pub definition: Option<String>,
303 pub is_materialized: bool,
305 pub columns: Vec<ColumnInfo>,
307}
308
309#[derive(Debug, Clone, Default, Serialize, Deserialize)]
311pub struct EnumInfo {
312 pub name: String,
314 pub schema: Option<String>,
316 pub values: Vec<String>,
318}
319
320#[derive(Debug, Clone, Default, Serialize, Deserialize)]
322pub struct SequenceInfo {
323 pub name: String,
325 pub schema: Option<String>,
327 pub start: i64,
329 pub increment: i64,
331 pub min_value: Option<i64>,
333 pub max_value: Option<i64>,
335 pub cycle: bool,
337}
338
339pub mod queries {
345 use super::*;
346
347 pub fn tables_query(db_type: DatabaseType, schema: Option<&str>) -> String {
349 match db_type {
350 DatabaseType::PostgreSQL => {
351 let schema_filter = schema.unwrap_or("public");
352 format!(
353 "SELECT table_name, obj_description((quote_ident(table_schema) || '.' || quote_ident(table_name))::regclass) as comment \
354 FROM information_schema.tables \
355 WHERE table_schema = '{}' AND table_type = 'BASE TABLE' \
356 ORDER BY table_name",
357 schema_filter
358 )
359 }
360 DatabaseType::MySQL => {
361 let schema_filter = schema
362 .map(|s| format!("AND table_schema = '{}'", s))
363 .unwrap_or_default();
364 format!(
365 "SELECT table_name, table_comment as comment \
366 FROM information_schema.tables \
367 WHERE table_type = 'BASE TABLE' {} \
368 ORDER BY table_name",
369 schema_filter
370 )
371 }
372 DatabaseType::SQLite => "SELECT name as table_name, NULL as comment \
373 FROM sqlite_master \
374 WHERE type = 'table' AND name NOT LIKE 'sqlite_%' \
375 ORDER BY name"
376 .to_string(),
377 DatabaseType::MSSQL => {
378 let schema_filter = schema.unwrap_or("dbo");
379 format!(
380 "SELECT t.name as table_name, ep.value as comment \
381 FROM sys.tables t \
382 LEFT JOIN sys.extended_properties ep ON ep.major_id = t.object_id AND ep.minor_id = 0 AND ep.name = 'MS_Description' \
383 JOIN sys.schemas s ON t.schema_id = s.schema_id \
384 WHERE s.name = '{}' \
385 ORDER BY t.name",
386 schema_filter
387 )
388 }
389 }
390 }
391
392 pub fn columns_query(db_type: DatabaseType, table: &str, schema: Option<&str>) -> String {
394 match db_type {
395 DatabaseType::PostgreSQL => {
396 let schema_filter = schema.unwrap_or("public");
397 format!(
398 "SELECT \
399 c.column_name, \
400 c.data_type, \
401 c.udt_name, \
402 c.is_nullable = 'YES' as nullable, \
403 c.column_default, \
404 c.character_maximum_length, \
405 c.numeric_precision, \
406 c.numeric_scale, \
407 col_description((quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))::regclass, c.ordinal_position) as comment, \
408 CASE WHEN c.column_default LIKE 'nextval%' THEN true ELSE false END as auto_increment \
409 FROM information_schema.columns c \
410 WHERE c.table_schema = '{}' AND c.table_name = '{}' \
411 ORDER BY c.ordinal_position",
412 schema_filter, table
413 )
414 }
415 DatabaseType::MySQL => {
416 format!(
417 "SELECT \
418 column_name, \
419 data_type, \
420 column_type as udt_name, \
421 is_nullable = 'YES' as nullable, \
422 column_default, \
423 character_maximum_length, \
424 numeric_precision, \
425 numeric_scale, \
426 column_comment as comment, \
427 extra LIKE '%auto_increment%' as auto_increment \
428 FROM information_schema.columns \
429 WHERE table_name = '{}' {} \
430 ORDER BY ordinal_position",
431 table,
432 schema
433 .map(|s| format!("AND table_schema = '{}'", s))
434 .unwrap_or_default()
435 )
436 }
437 DatabaseType::SQLite => {
438 format!("PRAGMA table_info('{}')", table)
439 }
440 DatabaseType::MSSQL => {
441 let schema_filter = schema.unwrap_or("dbo");
442 format!(
443 "SELECT \
444 c.name as column_name, \
445 t.name as data_type, \
446 t.name as udt_name, \
447 c.is_nullable as nullable, \
448 dc.definition as column_default, \
449 c.max_length as character_maximum_length, \
450 c.precision as numeric_precision, \
451 c.scale as numeric_scale, \
452 ep.value as comment, \
453 c.is_identity as auto_increment \
454 FROM sys.columns c \
455 JOIN sys.types t ON c.user_type_id = t.user_type_id \
456 JOIN sys.tables tb ON c.object_id = tb.object_id \
457 JOIN sys.schemas s ON tb.schema_id = s.schema_id \
458 LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id \
459 LEFT JOIN sys.extended_properties ep ON ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.name = 'MS_Description' \
460 WHERE tb.name = '{}' AND s.name = '{}' \
461 ORDER BY c.column_id",
462 table, schema_filter
463 )
464 }
465 }
466 }
467
468 pub fn primary_keys_query(db_type: DatabaseType, table: &str, schema: Option<&str>) -> String {
470 match db_type {
471 DatabaseType::PostgreSQL => {
472 let schema_filter = schema.unwrap_or("public");
473 format!(
474 "SELECT a.attname as column_name \
475 FROM pg_index i \
476 JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) \
477 JOIN pg_class c ON c.oid = i.indrelid \
478 JOIN pg_namespace n ON n.oid = c.relnamespace \
479 WHERE i.indisprimary AND c.relname = '{}' AND n.nspname = '{}' \
480 ORDER BY array_position(i.indkey, a.attnum)",
481 table, schema_filter
482 )
483 }
484 DatabaseType::MySQL => {
485 format!(
486 "SELECT column_name \
487 FROM information_schema.key_column_usage \
488 WHERE constraint_name = 'PRIMARY' AND table_name = '{}' {} \
489 ORDER BY ordinal_position",
490 table,
491 schema
492 .map(|s| format!("AND table_schema = '{}'", s))
493 .unwrap_or_default()
494 )
495 }
496 DatabaseType::SQLite => {
497 format!("PRAGMA table_info('{}')", table) }
499 DatabaseType::MSSQL => {
500 let schema_filter = schema.unwrap_or("dbo");
501 format!(
502 "SELECT c.name as column_name \
503 FROM sys.indexes i \
504 JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id \
505 JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id \
506 JOIN sys.tables t ON i.object_id = t.object_id \
507 JOIN sys.schemas s ON t.schema_id = s.schema_id \
508 WHERE i.is_primary_key = 1 AND t.name = '{}' AND s.name = '{}' \
509 ORDER BY ic.key_ordinal",
510 table, schema_filter
511 )
512 }
513 }
514 }
515
516 pub fn foreign_keys_query(db_type: DatabaseType, table: &str, schema: Option<&str>) -> String {
518 match db_type {
519 DatabaseType::PostgreSQL => {
520 let schema_filter = schema.unwrap_or("public");
521 format!(
522 "SELECT \
523 tc.constraint_name, \
524 kcu.column_name, \
525 ccu.table_name as referenced_table, \
526 ccu.table_schema as referenced_schema, \
527 ccu.column_name as referenced_column, \
528 rc.delete_rule, \
529 rc.update_rule \
530 FROM information_schema.table_constraints tc \
531 JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name \
532 JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name \
533 JOIN information_schema.referential_constraints rc ON rc.constraint_name = tc.constraint_name \
534 WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = '{}' AND tc.table_schema = '{}' \
535 ORDER BY tc.constraint_name, kcu.ordinal_position",
536 table, schema_filter
537 )
538 }
539 DatabaseType::MySQL => {
540 format!(
541 "SELECT \
542 constraint_name, \
543 column_name, \
544 referenced_table_name as referenced_table, \
545 referenced_table_schema as referenced_schema, \
546 referenced_column_name as referenced_column, \
547 'NO ACTION' as delete_rule, \
548 'NO ACTION' as update_rule \
549 FROM information_schema.key_column_usage \
550 WHERE referenced_table_name IS NOT NULL AND table_name = '{}' {} \
551 ORDER BY constraint_name, ordinal_position",
552 table,
553 schema
554 .map(|s| format!("AND table_schema = '{}'", s))
555 .unwrap_or_default()
556 )
557 }
558 DatabaseType::SQLite => {
559 format!("PRAGMA foreign_key_list('{}')", table)
560 }
561 DatabaseType::MSSQL => {
562 let schema_filter = schema.unwrap_or("dbo");
563 format!(
564 "SELECT \
565 fk.name as constraint_name, \
566 c.name as column_name, \
567 rt.name as referenced_table, \
568 rs.name as referenced_schema, \
569 rc.name as referenced_column, \
570 fk.delete_referential_action_desc as delete_rule, \
571 fk.update_referential_action_desc as update_rule \
572 FROM sys.foreign_keys fk \
573 JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id \
574 JOIN sys.columns c ON fkc.parent_object_id = c.object_id AND fkc.parent_column_id = c.column_id \
575 JOIN sys.tables t ON fk.parent_object_id = t.object_id \
576 JOIN sys.schemas s ON t.schema_id = s.schema_id \
577 JOIN sys.tables rt ON fk.referenced_object_id = rt.object_id \
578 JOIN sys.schemas rs ON rt.schema_id = rs.schema_id \
579 JOIN sys.columns rc ON fkc.referenced_object_id = rc.object_id AND fkc.referenced_column_id = rc.column_id \
580 WHERE t.name = '{}' AND s.name = '{}' \
581 ORDER BY fk.name",
582 table, schema_filter
583 )
584 }
585 }
586 }
587
588 pub fn indexes_query(db_type: DatabaseType, table: &str, schema: Option<&str>) -> String {
590 match db_type {
591 DatabaseType::PostgreSQL => {
592 let schema_filter = schema.unwrap_or("public");
593 format!(
594 "SELECT \
595 i.relname as index_name, \
596 a.attname as column_name, \
597 ix.indisunique as is_unique, \
598 ix.indisprimary as is_primary, \
599 am.amname as index_type, \
600 pg_get_expr(ix.indpred, ix.indrelid) as filter \
601 FROM pg_index ix \
602 JOIN pg_class t ON t.oid = ix.indrelid \
603 JOIN pg_class i ON i.oid = ix.indexrelid \
604 JOIN pg_namespace n ON n.oid = t.relnamespace \
605 JOIN pg_am am ON i.relam = am.oid \
606 JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) \
607 WHERE t.relname = '{}' AND n.nspname = '{}' \
608 ORDER BY i.relname, array_position(ix.indkey, a.attnum)",
609 table, schema_filter
610 )
611 }
612 DatabaseType::MySQL => {
613 format!(
614 "SELECT \
615 index_name, \
616 column_name, \
617 NOT non_unique as is_unique, \
618 index_name = 'PRIMARY' as is_primary, \
619 index_type, \
620 NULL as filter \
621 FROM information_schema.statistics \
622 WHERE table_name = '{}' {} \
623 ORDER BY index_name, seq_in_index",
624 table,
625 schema
626 .map(|s| format!("AND table_schema = '{}'", s))
627 .unwrap_or_default()
628 )
629 }
630 DatabaseType::SQLite => {
631 format!("PRAGMA index_list('{}')", table)
632 }
633 DatabaseType::MSSQL => {
634 let schema_filter = schema.unwrap_or("dbo");
635 format!(
636 "SELECT \
637 i.name as index_name, \
638 c.name as column_name, \
639 i.is_unique, \
640 i.is_primary_key as is_primary, \
641 i.type_desc as index_type, \
642 i.filter_definition as filter \
643 FROM sys.indexes i \
644 JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id \
645 JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id \
646 JOIN sys.tables t ON i.object_id = t.object_id \
647 JOIN sys.schemas s ON t.schema_id = s.schema_id \
648 WHERE t.name = '{}' AND s.name = '{}' AND i.name IS NOT NULL \
649 ORDER BY i.name, ic.key_ordinal",
650 table, schema_filter
651 )
652 }
653 }
654 }
655
656 pub fn enums_query(schema: Option<&str>) -> String {
658 let schema_filter = schema.unwrap_or("public");
659 format!(
660 "SELECT t.typname as enum_name, e.enumlabel as enum_value \
661 FROM pg_type t \
662 JOIN pg_enum e ON t.oid = e.enumtypid \
663 JOIN pg_namespace n ON n.oid = t.typnamespace \
664 WHERE n.nspname = '{}' \
665 ORDER BY t.typname, e.enumsortorder",
666 schema_filter
667 )
668 }
669
670 pub fn views_query(db_type: DatabaseType, schema: Option<&str>) -> String {
672 match db_type {
673 DatabaseType::PostgreSQL => {
674 let schema_filter = schema.unwrap_or("public");
675 format!(
676 "SELECT table_name as view_name, view_definition, false as is_materialized \
677 FROM information_schema.views \
678 WHERE table_schema = '{}' \
679 UNION ALL \
680 SELECT matviewname as view_name, definition as view_definition, true as is_materialized \
681 FROM pg_matviews \
682 WHERE schemaname = '{}' \
683 ORDER BY view_name",
684 schema_filter, schema_filter
685 )
686 }
687 DatabaseType::MySQL => {
688 format!(
689 "SELECT table_name as view_name, view_definition, false as is_materialized \
690 FROM information_schema.views \
691 WHERE table_schema = '{}' \
692 ORDER BY view_name",
693 schema.unwrap_or("information_schema")
694 )
695 }
696 DatabaseType::SQLite => {
697 "SELECT name as view_name, sql as view_definition, 0 as is_materialized \
698 FROM sqlite_master \
699 WHERE type = 'view' \
700 ORDER BY name"
701 .to_string()
702 }
703 DatabaseType::MSSQL => {
704 let schema_filter = schema.unwrap_or("dbo");
705 format!(
706 "SELECT v.name as view_name, m.definition as view_definition, \
707 CASE WHEN i.object_id IS NOT NULL THEN 1 ELSE 0 END as is_materialized \
708 FROM sys.views v \
709 JOIN sys.schemas s ON v.schema_id = s.schema_id \
710 JOIN sys.sql_modules m ON v.object_id = m.object_id \
711 LEFT JOIN sys.indexes i ON v.object_id = i.object_id AND i.index_id = 1 \
712 WHERE s.name = '{}' \
713 ORDER BY v.name",
714 schema_filter
715 )
716 }
717 }
718 }
719}
720
721pub fn normalize_type(
727 db_type: DatabaseType,
728 type_name: &str,
729 max_length: Option<i32>,
730 precision: Option<i32>,
731 scale: Option<i32>,
732) -> NormalizedType {
733 let type_lower = type_name.to_lowercase();
734
735 match db_type {
736 DatabaseType::PostgreSQL => {
737 normalize_postgres_type(&type_lower, max_length, precision, scale)
738 }
739 DatabaseType::MySQL => normalize_mysql_type(&type_lower, max_length, precision, scale),
740 DatabaseType::SQLite => normalize_sqlite_type(&type_lower),
741 DatabaseType::MSSQL => normalize_mssql_type(&type_lower, max_length, precision, scale),
742 }
743}
744
745fn normalize_postgres_type(
746 type_name: &str,
747 _max_length: Option<i32>,
748 precision: Option<i32>,
749 scale: Option<i32>,
750) -> NormalizedType {
751 match type_name {
752 "int2" | "smallint" | "smallserial" => NormalizedType::SmallInt,
753 "int4" | "integer" | "int" | "serial" => NormalizedType::Int,
754 "int8" | "bigint" | "bigserial" => NormalizedType::BigInt,
755 "real" | "float4" => NormalizedType::Float,
756 "double precision" | "float8" => NormalizedType::Double,
757 "numeric" | "decimal" => NormalizedType::Decimal { precision, scale },
758 "bool" | "boolean" => NormalizedType::Boolean,
759 "text" => NormalizedType::Text,
760 "varchar" | "character varying" => NormalizedType::VarChar {
761 length: _max_length,
762 },
763 "char" | "character" | "bpchar" => NormalizedType::Char {
764 length: _max_length,
765 },
766 "bytea" => NormalizedType::Bytes,
767 "timestamp" | "timestamp without time zone" => NormalizedType::Timestamp,
768 "timestamptz" | "timestamp with time zone" => NormalizedType::DateTime,
769 "date" => NormalizedType::Date,
770 "time" | "time without time zone" | "timetz" | "time with time zone" => {
771 NormalizedType::Time
772 }
773 "json" | "jsonb" => NormalizedType::Json,
774 "uuid" => NormalizedType::Uuid,
775 t if t.ends_with("[]") => {
776 let inner = normalize_postgres_type(&t[..t.len() - 2], None, None, None);
777 NormalizedType::Array(Box::new(inner))
778 }
779 t => NormalizedType::Unknown(t.to_string()),
780 }
781}
782
783fn normalize_mysql_type(
784 type_name: &str,
785 max_length: Option<i32>,
786 precision: Option<i32>,
787 scale: Option<i32>,
788) -> NormalizedType {
789 match type_name {
790 "tinyint" | "smallint" => NormalizedType::SmallInt,
791 "int" | "integer" | "mediumint" => NormalizedType::Int,
792 "bigint" => NormalizedType::BigInt,
793 "float" => NormalizedType::Float,
794 "double" | "real" => NormalizedType::Double,
795 "decimal" | "numeric" => NormalizedType::Decimal { precision, scale },
796 "bit" | "bool" | "boolean" => NormalizedType::Boolean,
797 "text" | "mediumtext" | "longtext" => NormalizedType::Text,
798 "varchar" => NormalizedType::VarChar { length: max_length },
799 "char" => NormalizedType::Char { length: max_length },
800 "tinyblob" | "blob" | "mediumblob" | "longblob" | "binary" | "varbinary" => {
801 NormalizedType::Bytes
802 }
803 "datetime" | "timestamp" => NormalizedType::DateTime,
804 "date" => NormalizedType::Date,
805 "time" => NormalizedType::Time,
806 "json" => NormalizedType::Json,
807 t if t.starts_with("enum(") => {
808 NormalizedType::Enum(t.to_string())
810 }
811 t => NormalizedType::Unknown(t.to_string()),
812 }
813}
814
815fn normalize_sqlite_type(type_name: &str) -> NormalizedType {
816 match type_name {
818 "integer" | "int" => NormalizedType::Int,
819 "real" | "float" | "double" => NormalizedType::Double,
820 "text" | "varchar" | "char" | "clob" => NormalizedType::Text,
821 "blob" => NormalizedType::Bytes,
822 "boolean" | "bool" => NormalizedType::Boolean,
823 "datetime" | "timestamp" | "date" | "time" => NormalizedType::DateTime,
824 t => NormalizedType::Unknown(t.to_string()),
825 }
826}
827
828fn normalize_mssql_type(
829 type_name: &str,
830 max_length: Option<i32>,
831 precision: Option<i32>,
832 scale: Option<i32>,
833) -> NormalizedType {
834 match type_name {
835 "tinyint" | "smallint" => NormalizedType::SmallInt,
836 "int" => NormalizedType::Int,
837 "bigint" => NormalizedType::BigInt,
838 "real" | "float" => NormalizedType::Float,
839 "decimal" | "numeric" | "money" | "smallmoney" => {
840 NormalizedType::Decimal { precision, scale }
841 }
842 "bit" => NormalizedType::Boolean,
843 "text" | "ntext" => NormalizedType::Text,
844 "varchar" | "nvarchar" => NormalizedType::VarChar { length: max_length },
845 "char" | "nchar" => NormalizedType::Char { length: max_length },
846 "binary" | "varbinary" | "image" => NormalizedType::Bytes,
847 "datetime" | "datetime2" | "datetimeoffset" | "smalldatetime" => NormalizedType::DateTime,
848 "date" => NormalizedType::Date,
849 "time" => NormalizedType::Time,
850 "uniqueidentifier" => NormalizedType::Uuid,
851 t => NormalizedType::Unknown(t.to_string()),
852 }
853}
854
855pub fn generate_prax_schema(db: &DatabaseSchema) -> String {
861 let mut output = String::new();
862
863 output.push_str("// Generated by Prax introspection\n");
865 output.push_str(&format!("// Database: {}\n\n", db.name));
866
867 for enum_info in &db.enums {
869 output.push_str(&generate_enum(enum_info));
870 output.push('\n');
871 }
872
873 for table in &db.tables {
875 output.push_str(&generate_model(table, &db.tables));
876 output.push('\n');
877 }
878
879 for view in &db.views {
881 output.push_str(&generate_view(view));
882 output.push('\n');
883 }
884
885 output
886}
887
888fn generate_enum(enum_info: &EnumInfo) -> String {
889 let mut output = format!("enum {} {{\n", enum_info.name);
890 for value in &enum_info.values {
891 output.push_str(&format!(" {}\n", value));
892 }
893 output.push_str("}\n");
894 output
895}
896
897fn generate_model(table: &TableInfo, all_tables: &[TableInfo]) -> String {
898 let mut output = String::new();
899
900 if let Some(ref comment) = table.comment {
902 output.push_str(&format!("/// {}\n", comment));
903 }
904
905 output.push_str(&format!("model {} {{\n", pascal_case(&table.name)));
906
907 for col in &table.columns {
909 output.push_str(&generate_field(col, &table.primary_key));
910 }
911
912 for fk in &table.foreign_keys {
914 output.push_str(&generate_relation(fk, all_tables));
915 }
916
917 let attrs = generate_model_attributes(table);
919 if !attrs.is_empty() {
920 output.push('\n');
921 output.push_str(&attrs);
922 }
923
924 output.push_str("}\n");
925 output
926}
927
928fn generate_field(col: &ColumnInfo, primary_key: &[String]) -> String {
929 let mut attrs = Vec::new();
930
931 if primary_key.contains(&col.name) {
933 attrs.push("@id".to_string());
934 }
935
936 if col.auto_increment {
938 attrs.push("@auto".to_string());
939 }
940
941 if col.is_unique && !primary_key.contains(&col.name) {
943 attrs.push("@unique".to_string());
944 }
945
946 if let Some(ref default) = col.default {
948 if !col.auto_increment {
949 let default_val = simplify_default(default);
950 attrs.push(format!("@default({})", default_val));
951 }
952 }
953
954 let field_name = camel_case(&col.name);
956 if field_name != col.name {
957 attrs.push(format!("@map(\"{}\")", col.name));
958 }
959
960 let type_str = col.normalized_type.to_prax_type();
962 let optional = if col.nullable { "?" } else { "" };
963
964 let attrs_str = if attrs.is_empty() {
965 String::new()
966 } else {
967 format!(" {}", attrs.join(" "))
968 };
969
970 format!(" {} {}{}{}\n", field_name, type_str, optional, attrs_str)
971}
972
973fn generate_relation(fk: &ForeignKeyInfo, all_tables: &[TableInfo]) -> String {
974 let _ref_table = all_tables.iter().find(|t| t.name == fk.referenced_table);
976 let ref_name = pascal_case(&fk.referenced_table);
977
978 let field_name = if fk.columns.len() == 1 {
979 let col = &fk.columns[0];
981 if col.ends_with("_id") {
982 camel_case(&col[..col.len() - 3])
983 } else {
984 camel_case(&fk.referenced_table)
985 }
986 } else {
987 camel_case(&fk.referenced_table)
988 };
989
990 let mut attrs = vec![format!(
991 "@relation(fields: [{}], references: [{}]",
992 fk.columns
993 .iter()
994 .map(|c| camel_case(c))
995 .collect::<Vec<_>>()
996 .join(", "),
997 fk.referenced_columns
998 .iter()
999 .map(|c| camel_case(c))
1000 .collect::<Vec<_>>()
1001 .join(", ")
1002 )];
1003
1004 if fk.on_delete != ReferentialAction::NoAction {
1006 attrs[0].push_str(&format!(", onDelete: {}", fk.on_delete.to_prax()));
1007 }
1008 if fk.on_update != ReferentialAction::NoAction {
1009 attrs[0].push_str(&format!(", onUpdate: {}", fk.on_update.to_prax()));
1010 }
1011
1012 attrs[0].push(')');
1013
1014 format!(" {} {} {}\n", field_name, ref_name, attrs.join(" "))
1015}
1016
1017fn generate_model_attributes(table: &TableInfo) -> String {
1018 let mut output = String::new();
1019
1020 let model_name = pascal_case(&table.name);
1022 if model_name.to_lowercase() != table.name.to_lowercase() {
1023 output.push_str(&format!(" @@map(\"{}\")\n", table.name));
1024 }
1025
1026 if table.primary_key.len() > 1 {
1028 let fields: Vec<_> = table.primary_key.iter().map(|c| camel_case(c)).collect();
1029 output.push_str(&format!(" @@id([{}])\n", fields.join(", ")));
1030 }
1031
1032 for idx in &table.indexes {
1034 if !idx.is_primary {
1035 let cols: Vec<_> = idx.columns.iter().map(|c| camel_case(&c.name)).collect();
1036 if idx.is_unique {
1037 output.push_str(&format!(" @@unique([{}])\n", cols.join(", ")));
1038 } else {
1039 output.push_str(&format!(" @@index([{}])\n", cols.join(", ")));
1040 }
1041 }
1042 }
1043
1044 output
1045}
1046
1047fn generate_view(view: &ViewInfo) -> String {
1048 let mut output = String::new();
1049
1050 let keyword = if view.is_materialized {
1051 "materializedView"
1052 } else {
1053 "view"
1054 };
1055 output.push_str(&format!("{} {} {{\n", keyword, pascal_case(&view.name)));
1056
1057 for col in &view.columns {
1058 let type_str = col.normalized_type.to_prax_type();
1059 let optional = if col.nullable { "?" } else { "" };
1060 output.push_str(&format!(
1061 " {} {}{}\n",
1062 camel_case(&col.name),
1063 type_str,
1064 optional
1065 ));
1066 }
1067
1068 if let Some(ref def) = view.definition {
1069 output.push_str(&format!("\n @@sql(\"{}\")\n", def.replace('"', "\\\"")));
1070 }
1071
1072 output.push_str("}\n");
1073 output
1074}
1075
1076pub mod mongodb {
1082 use serde_json::Value as JsonValue;
1083
1084 use super::{ColumnInfo, NormalizedType, TableInfo};
1085
1086 #[derive(Debug, Clone, Default)]
1088 pub struct SchemaInferrer {
1089 pub fields: std::collections::HashMap<String, FieldSchema>,
1091 pub samples: usize,
1093 }
1094
1095 #[derive(Debug, Clone, Default)]
1097 pub struct FieldSchema {
1098 pub name: String,
1100 pub types: Vec<String>,
1102 pub required: bool,
1104 pub nested: Option<Box<SchemaInferrer>>,
1106 pub array_type: Option<String>,
1108 }
1109
1110 impl SchemaInferrer {
1111 pub fn new() -> Self {
1113 Self::default()
1114 }
1115
1116 pub fn add_document(&mut self, doc: &JsonValue) {
1118 self.samples += 1;
1119
1120 if let Some(obj) = doc.as_object() {
1121 for (key, value) in obj {
1122 self.infer_field(key, value);
1123 }
1124 }
1125 }
1126
1127 fn infer_field(&mut self, name: &str, value: &JsonValue) {
1128 let field = self
1129 .fields
1130 .entry(name.to_string())
1131 .or_insert_with(|| FieldSchema {
1132 name: name.to_string(),
1133 required: true,
1134 ..Default::default()
1135 });
1136
1137 let type_name = match value {
1138 JsonValue::Null => "null",
1139 JsonValue::Bool(_) => "boolean",
1140 JsonValue::Number(n) if n.is_i64() => "int",
1141 JsonValue::Number(n) if n.is_f64() => "double",
1142 JsonValue::Number(_) => "number",
1143 JsonValue::String(s) => {
1144 if s.len() == 24 && s.chars().all(|c| c.is_ascii_hexdigit()) {
1146 "objectId"
1147 } else if is_iso_datetime(s) {
1148 "date"
1149 } else {
1150 "string"
1151 }
1152 }
1153 JsonValue::Array(arr) => {
1154 if let Some(first) = arr.first() {
1155 let elem_type = match first {
1156 JsonValue::Object(_) => "object",
1157 JsonValue::String(_) => "string",
1158 JsonValue::Number(_) => "number",
1159 JsonValue::Bool(_) => "boolean",
1160 _ => "mixed",
1161 };
1162 field.array_type = Some(elem_type.to_string());
1163 }
1164 "array"
1165 }
1166 JsonValue::Object(_) => {
1167 let mut nested = field.nested.take().unwrap_or_default();
1169 nested.add_document(value);
1170 field.nested = Some(nested);
1171 "object"
1172 }
1173 };
1174
1175 if !field.types.contains(&type_name.to_string()) {
1176 field.types.push(type_name.to_string());
1177 }
1178 }
1179
1180 pub fn to_table_info(&self, collection_name: &str) -> TableInfo {
1182 let mut columns = Vec::new();
1183
1184 for (name, field) in &self.fields {
1185 let normalized = infer_normalized_type(field);
1186 columns.push(ColumnInfo {
1187 name: name.clone(),
1188 db_type: field.types.join("|"),
1189 normalized_type: normalized,
1190 nullable: !field.required || field.types.contains(&"null".to_string()),
1191 is_primary_key: name == "_id",
1192 ..Default::default()
1193 });
1194 }
1195
1196 TableInfo {
1197 name: collection_name.to_string(),
1198 columns,
1199 primary_key: vec!["_id".to_string()],
1200 ..Default::default()
1201 }
1202 }
1203 }
1204
1205 fn infer_normalized_type(field: &FieldSchema) -> NormalizedType {
1206 if field.types.contains(&"objectId".to_string()) {
1208 NormalizedType::String } else if field.types.contains(&"date".to_string()) {
1210 NormalizedType::DateTime
1211 } else if field.types.contains(&"boolean".to_string()) {
1212 NormalizedType::Boolean
1213 } else if field.types.contains(&"int".to_string()) {
1214 NormalizedType::Int
1215 } else if field.types.contains(&"double".to_string())
1216 || field.types.contains(&"number".to_string())
1217 {
1218 NormalizedType::Double
1219 } else if field.types.contains(&"array".to_string()) {
1220 let inner = match field.array_type.as_deref() {
1221 Some("string") => NormalizedType::String,
1222 Some("number") => NormalizedType::Double,
1223 Some("boolean") => NormalizedType::Boolean,
1224 _ => NormalizedType::Json,
1225 };
1226 NormalizedType::Array(Box::new(inner))
1227 } else if field.types.contains(&"object".to_string()) {
1228 NormalizedType::Json
1229 } else if field.types.contains(&"string".to_string()) {
1230 NormalizedType::String
1231 } else {
1232 NormalizedType::Unknown(field.types.join("|"))
1233 }
1234 }
1235
1236 pub fn list_indexes_command(collection: &str) -> JsonValue {
1238 serde_json::json!({
1239 "listIndexes": collection
1240 })
1241 }
1242
1243 pub fn list_collections_command() -> JsonValue {
1245 serde_json::json!({
1246 "listCollections": 1
1247 })
1248 }
1249
1250 fn is_iso_datetime(s: &str) -> bool {
1252 if s.len() < 10 {
1254 return false;
1255 }
1256
1257 let bytes = s.as_bytes();
1258 bytes.get(4) == Some(&b'-')
1260 && bytes.get(7) == Some(&b'-')
1261 && bytes[0..4].iter().all(|b| b.is_ascii_digit())
1262 && bytes[5..7].iter().all(|b| b.is_ascii_digit())
1263 && bytes[8..10].iter().all(|b| b.is_ascii_digit())
1264 }
1265}
1266
1267fn pascal_case(s: &str) -> String {
1272 s.split('_')
1273 .map(|part| {
1274 let mut chars = part.chars();
1275 match chars.next() {
1276 None => String::new(),
1277 Some(c) => c.to_uppercase().chain(chars).collect(),
1278 }
1279 })
1280 .collect()
1281}
1282
1283fn camel_case(s: &str) -> String {
1284 let pascal = pascal_case(s);
1285 let mut chars = pascal.chars();
1286 match chars.next() {
1287 None => String::new(),
1288 Some(c) => c.to_lowercase().chain(chars).collect(),
1289 }
1290}
1291
1292fn simplify_default(default: &str) -> String {
1293 let d = default.trim();
1295
1296 if d.eq_ignore_ascii_case("now()") || d.eq_ignore_ascii_case("current_timestamp") {
1297 return "now()".to_string();
1298 }
1299
1300 if d.starts_with("'") && d.ends_with("'") {
1301 return format!("\"{}\"", &d[1..d.len() - 1]);
1302 }
1303
1304 if d.eq_ignore_ascii_case("true") || d.eq_ignore_ascii_case("false") {
1305 return d.to_lowercase();
1306 }
1307
1308 if d.parse::<i64>().is_ok() || d.parse::<f64>().is_ok() {
1309 return d.to_string();
1310 }
1311
1312 format!("dbgenerated(\"{}\")", d.replace('"', "\\\""))
1313}
1314
1315#[cfg(test)]
1316mod tests {
1317 use super::*;
1318
1319 #[test]
1320 fn test_pascal_case() {
1321 assert_eq!(pascal_case("user_profile"), "UserProfile");
1322 assert_eq!(pascal_case("id"), "Id");
1323 assert_eq!(pascal_case("created_at"), "CreatedAt");
1324 }
1325
1326 #[test]
1327 fn test_camel_case() {
1328 assert_eq!(camel_case("user_profile"), "userProfile");
1329 assert_eq!(camel_case("ID"), "iD");
1330 assert_eq!(camel_case("created_at"), "createdAt");
1331 }
1332
1333 #[test]
1334 fn test_normalize_postgres_type() {
1335 assert_eq!(
1336 normalize_postgres_type("int4", None, None, None),
1337 NormalizedType::Int
1338 );
1339 assert_eq!(
1340 normalize_postgres_type("bigint", None, None, None),
1341 NormalizedType::BigInt
1342 );
1343 assert_eq!(
1344 normalize_postgres_type("text", None, None, None),
1345 NormalizedType::Text
1346 );
1347 assert_eq!(
1348 normalize_postgres_type("timestamptz", None, None, None),
1349 NormalizedType::DateTime
1350 );
1351 assert_eq!(
1352 normalize_postgres_type("jsonb", None, None, None),
1353 NormalizedType::Json
1354 );
1355 assert_eq!(
1356 normalize_postgres_type("uuid", None, None, None),
1357 NormalizedType::Uuid
1358 );
1359 }
1360
1361 #[test]
1362 fn test_normalize_mysql_type() {
1363 assert_eq!(
1364 normalize_mysql_type("int", None, None, None),
1365 NormalizedType::Int
1366 );
1367 assert_eq!(
1368 normalize_mysql_type("varchar", Some(255), None, None),
1369 NormalizedType::VarChar { length: Some(255) }
1370 );
1371 assert_eq!(
1372 normalize_mysql_type("datetime", None, None, None),
1373 NormalizedType::DateTime
1374 );
1375 }
1376
1377 #[test]
1378 fn test_referential_action() {
1379 assert_eq!(
1380 ReferentialAction::from_str("CASCADE"),
1381 ReferentialAction::Cascade
1382 );
1383 assert_eq!(
1384 ReferentialAction::from_str("SET NULL"),
1385 ReferentialAction::SetNull
1386 );
1387 assert_eq!(
1388 ReferentialAction::from_str("NO ACTION"),
1389 ReferentialAction::NoAction
1390 );
1391 }
1392
1393 #[test]
1394 fn test_generate_simple_model() {
1395 let table = TableInfo {
1396 name: "users".to_string(),
1397 columns: vec![
1398 ColumnInfo {
1399 name: "id".to_string(),
1400 normalized_type: NormalizedType::Int,
1401 auto_increment: true,
1402 ..Default::default()
1403 },
1404 ColumnInfo {
1405 name: "email".to_string(),
1406 normalized_type: NormalizedType::String,
1407 is_unique: true,
1408 ..Default::default()
1409 },
1410 ColumnInfo {
1411 name: "created_at".to_string(),
1412 normalized_type: NormalizedType::DateTime,
1413 nullable: true,
1414 default: Some("now()".to_string()),
1415 ..Default::default()
1416 },
1417 ],
1418 primary_key: vec!["id".to_string()],
1419 ..Default::default()
1420 };
1421
1422 let schema = generate_model(&table, &[]);
1423 assert!(schema.contains("model Users"));
1424 assert!(schema.contains("id Int @id @auto"));
1425 assert!(schema.contains("email String @unique"));
1426 assert!(schema.contains("createdAt DateTime?"));
1427 }
1428
1429 #[test]
1430 fn test_simplify_default() {
1431 assert_eq!(simplify_default("NOW()"), "now()");
1432 assert_eq!(simplify_default("CURRENT_TIMESTAMP"), "now()");
1433 assert_eq!(simplify_default("'hello'"), "\"hello\"");
1434 assert_eq!(simplify_default("42"), "42");
1435 assert_eq!(simplify_default("true"), "true");
1436 }
1437
1438 #[test]
1439 fn test_queries_tables() {
1440 let pg = queries::tables_query(DatabaseType::PostgreSQL, Some("public"));
1441 assert!(pg.contains("information_schema.tables"));
1442 assert!(pg.contains("public"));
1443
1444 let mysql = queries::tables_query(DatabaseType::MySQL, None);
1445 assert!(mysql.contains("information_schema.tables"));
1446
1447 let sqlite = queries::tables_query(DatabaseType::SQLite, None);
1448 assert!(sqlite.contains("sqlite_master"));
1449 }
1450
1451 mod mongodb_tests {
1452 use super::super::mongodb::*;
1453
1454 #[test]
1455 fn test_schema_inferrer() {
1456 let mut inferrer = SchemaInferrer::new();
1457
1458 inferrer.add_document(&serde_json::json!({
1459 "_id": "507f1f77bcf86cd799439011",
1460 "name": "Alice",
1461 "age": 30,
1462 "active": true
1463 }));
1464
1465 inferrer.add_document(&serde_json::json!({
1466 "_id": "507f1f77bcf86cd799439012",
1467 "name": "Bob",
1468 "age": 25,
1469 "active": false,
1470 "email": "bob@example.com"
1471 }));
1472
1473 let table = inferrer.to_table_info("users");
1474 assert_eq!(table.name, "users");
1475 assert!(table.columns.iter().any(|c| c.name == "_id"));
1476 assert!(table.columns.iter().any(|c| c.name == "name"));
1477 assert!(table.columns.iter().any(|c| c.name == "age"));
1478 }
1479 }
1480}