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