prax_query/
introspection.rs

1//! Database introspection and schema generation.
2//!
3//! This module provides types for introspecting existing databases and generating
4//! Prax schema definitions from the discovered structure.
5//!
6//! # Database Support
7//!
8//! | Feature              | PostgreSQL | MySQL | SQLite | MSSQL | MongoDB       |
9//! |----------------------|------------|-------|--------|-------|---------------|
10//! | Table introspection  | ✅         | ✅    | ✅     | ✅    | ✅ Collection |
11//! | Column types         | ✅         | ✅    | ✅     | ✅    | ✅ Inferred   |
12//! | Primary keys         | ✅         | ✅    | ✅     | ✅    | ✅ _id        |
13//! | Foreign keys         | ✅         | ✅    | ✅     | ✅    | ❌            |
14//! | Indexes              | ✅         | ✅    | ✅     | ✅    | ✅            |
15//! | Unique constraints   | ✅         | ✅    | ✅     | ✅    | ✅            |
16//! | Default values       | ✅         | ✅    | ✅     | ✅    | ❌            |
17//! | Enums                | ✅         | ✅    | ❌     | ❌    | ❌            |
18//! | Views                | ✅         | ✅    | ✅     | ✅    | ✅            |
19
20use serde::{Deserialize, Serialize};
21
22use crate::sql::DatabaseType;
23
24// ============================================================================
25// Introspection Results
26// ============================================================================
27
28/// Complete introspection result for a database.
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct DatabaseSchema {
31    /// Database name.
32    pub name: String,
33    /// Schema/namespace (PostgreSQL, MSSQL).
34    pub schema: Option<String>,
35    /// Tables discovered.
36    pub tables: Vec<TableInfo>,
37    /// Views discovered.
38    pub views: Vec<ViewInfo>,
39    /// Enums discovered.
40    pub enums: Vec<EnumInfo>,
41    /// Sequences discovered.
42    pub sequences: Vec<SequenceInfo>,
43}
44
45/// Information about a table.
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct TableInfo {
48    /// Table name.
49    pub name: String,
50    /// Schema/namespace.
51    pub schema: Option<String>,
52    /// Table comment/description.
53    pub comment: Option<String>,
54    /// Columns.
55    pub columns: Vec<ColumnInfo>,
56    /// Primary key columns.
57    pub primary_key: Vec<String>,
58    /// Foreign keys.
59    pub foreign_keys: Vec<ForeignKeyInfo>,
60    /// Indexes.
61    pub indexes: Vec<IndexInfo>,
62    /// Unique constraints.
63    pub unique_constraints: Vec<UniqueConstraint>,
64    /// Check constraints.
65    pub check_constraints: Vec<CheckConstraint>,
66}
67
68/// Information about a column.
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70pub struct ColumnInfo {
71    /// Column name.
72    pub name: String,
73    /// Database-specific type name.
74    pub db_type: String,
75    /// Normalized type for schema generation.
76    pub normalized_type: NormalizedType,
77    /// Whether the column is nullable.
78    pub nullable: bool,
79    /// Default value expression.
80    pub default: Option<String>,
81    /// Whether this is an auto-increment/serial column.
82    pub auto_increment: bool,
83    /// Whether this is part of primary key.
84    pub is_primary_key: bool,
85    /// Whether this column has a unique constraint.
86    pub is_unique: bool,
87    /// Column comment.
88    pub comment: Option<String>,
89    /// Character maximum length (for varchar, etc.).
90    pub max_length: Option<i32>,
91    /// Numeric precision.
92    pub precision: Option<i32>,
93    /// Numeric scale.
94    pub scale: Option<i32>,
95    /// Enum type name (if applicable).
96    pub enum_name: Option<String>,
97}
98
99/// Normalized type for cross-database compatibility.
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub enum NormalizedType {
102    /// Integer types.
103    Int,
104    BigInt,
105    SmallInt,
106    /// Floating point.
107    Float,
108    Double,
109    /// Fixed precision.
110    Decimal { precision: Option<i32>, scale: Option<i32> },
111    /// String types.
112    String,
113    Text,
114    Char { length: Option<i32> },
115    VarChar { length: Option<i32> },
116    /// Binary.
117    Bytes,
118    /// Boolean.
119    Boolean,
120    /// Date/time.
121    DateTime,
122    Date,
123    Time,
124    Timestamp,
125    /// JSON.
126    Json,
127    /// UUID.
128    Uuid,
129    /// Array of type.
130    Array(Box<NormalizedType>),
131    /// Enum reference.
132    Enum(String),
133    /// Unknown/unsupported.
134    Unknown(String),
135}
136
137impl Default for NormalizedType {
138    fn default() -> Self {
139        Self::Unknown("unknown".to_string())
140    }
141}
142
143impl NormalizedType {
144    /// Convert to Prax schema type string.
145    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(), // Or custom UUID type
161            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/// Information about a foreign key.
169#[derive(Debug, Clone, Default, Serialize, Deserialize)]
170pub struct ForeignKeyInfo {
171    /// Constraint name.
172    pub name: String,
173    /// Local columns.
174    pub columns: Vec<String>,
175    /// Referenced table.
176    pub referenced_table: String,
177    /// Referenced schema.
178    pub referenced_schema: Option<String>,
179    /// Referenced columns.
180    pub referenced_columns: Vec<String>,
181    /// ON DELETE action.
182    pub on_delete: ReferentialAction,
183    /// ON UPDATE action.
184    pub on_update: ReferentialAction,
185}
186
187/// Referential action for foreign keys.
188#[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    /// Convert to Prax schema string.
200    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    /// Parse from database string.
211    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/// Information about an index.
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
225pub struct IndexInfo {
226    /// Index name.
227    pub name: String,
228    /// Columns in the index.
229    pub columns: Vec<IndexColumn>,
230    /// Whether this is a unique index.
231    pub is_unique: bool,
232    /// Whether this is a primary key index.
233    pub is_primary: bool,
234    /// Index type (btree, hash, gin, etc.).
235    pub index_type: Option<String>,
236    /// Filter condition (partial index).
237    pub filter: Option<String>,
238}
239
240/// A column in an index.
241#[derive(Debug, Clone, Default, Serialize, Deserialize)]
242pub struct IndexColumn {
243    /// Column name.
244    pub name: String,
245    /// Sort order.
246    pub order: SortOrder,
247    /// Nulls position.
248    pub nulls: NullsOrder,
249}
250
251/// Sort order for index columns.
252#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
253pub enum SortOrder {
254    #[default]
255    Asc,
256    Desc,
257}
258
259/// Nulls ordering for index columns.
260#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
261pub enum NullsOrder {
262    #[default]
263    Last,
264    First,
265}
266
267/// Unique constraint information.
268#[derive(Debug, Clone, Default, Serialize, Deserialize)]
269pub struct UniqueConstraint {
270    /// Constraint name.
271    pub name: String,
272    /// Columns.
273    pub columns: Vec<String>,
274}
275
276/// Check constraint information.
277#[derive(Debug, Clone, Default, Serialize, Deserialize)]
278pub struct CheckConstraint {
279    /// Constraint name.
280    pub name: String,
281    /// Check expression.
282    pub expression: String,
283}
284
285/// Information about a view.
286#[derive(Debug, Clone, Default, Serialize, Deserialize)]
287pub struct ViewInfo {
288    /// View name.
289    pub name: String,
290    /// Schema.
291    pub schema: Option<String>,
292    /// View definition SQL.
293    pub definition: Option<String>,
294    /// Whether this is a materialized view.
295    pub is_materialized: bool,
296    /// Columns (inferred from definition).
297    pub columns: Vec<ColumnInfo>,
298}
299
300/// Information about an enum type.
301#[derive(Debug, Clone, Default, Serialize, Deserialize)]
302pub struct EnumInfo {
303    /// Enum type name.
304    pub name: String,
305    /// Schema.
306    pub schema: Option<String>,
307    /// Enum values.
308    pub values: Vec<String>,
309}
310
311/// Information about a sequence.
312#[derive(Debug, Clone, Default, Serialize, Deserialize)]
313pub struct SequenceInfo {
314    /// Sequence name.
315    pub name: String,
316    /// Schema.
317    pub schema: Option<String>,
318    /// Start value.
319    pub start: i64,
320    /// Increment.
321    pub increment: i64,
322    /// Minimum value.
323    pub min_value: Option<i64>,
324    /// Maximum value.
325    pub max_value: Option<i64>,
326    /// Whether it cycles.
327    pub cycle: bool,
328}
329
330// ============================================================================
331// Introspection Queries
332// ============================================================================
333
334/// SQL queries for database introspection.
335pub mod queries {
336    use super::*;
337
338    /// Get tables query.
339    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    /// Get columns query.
383    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    /// Get primary keys query.
457    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) // Filter pk column in result
484            }
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    /// Get foreign keys query.
503    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    /// Get indexes query.
573    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    /// Get enums query (PostgreSQL only).
639    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    /// Get views query.
653    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
702// ============================================================================
703// Type Mapping
704// ============================================================================
705
706/// Map database types to normalized types.
707pub 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            // Extract enum name from table context
764            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    // SQLite has dynamic typing, so we map by affinity
772    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
803// ============================================================================
804// Schema Generation
805// ============================================================================
806
807/// Generate Prax schema from introspection result.
808pub fn generate_prax_schema(db: &DatabaseSchema) -> String {
809    let mut output = String::new();
810
811    // Header comment
812    output.push_str("// Generated by Prax introspection\n");
813    output.push_str(&format!("// Database: {}\n\n", db.name));
814
815    // Generate enums
816    for enum_info in &db.enums {
817        output.push_str(&generate_enum(enum_info));
818        output.push('\n');
819    }
820
821    // Generate models
822    for table in &db.tables {
823        output.push_str(&generate_model(table, &db.tables));
824        output.push('\n');
825    }
826
827    // Generate views
828    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    // Comment
849    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    // Fields
856    for col in &table.columns {
857        output.push_str(&generate_field(col, &table.primary_key));
858    }
859
860    // Relations
861    for fk in &table.foreign_keys {
862        output.push_str(&generate_relation(fk, all_tables));
863    }
864
865    // Model attributes
866    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    // Check if primary key
880    if primary_key.contains(&col.name) {
881        attrs.push("@id".to_string());
882    }
883
884    // Auto increment
885    if col.auto_increment {
886        attrs.push("@auto".to_string());
887    }
888
889    // Unique
890    if col.is_unique && !primary_key.contains(&col.name) {
891        attrs.push("@unique".to_string());
892    }
893
894    // Default
895    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    // Map if name differs
903    let field_name = camel_case(&col.name);
904    if field_name != col.name {
905        attrs.push(format!("@map(\"{}\")", col.name));
906    }
907
908    // Build type string
909    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    // Find the referenced table
923    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        // Derive relation name from FK column (e.g., user_id -> user)
928        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    // Add referential actions if not default
945    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    // @@map if table name differs from model name
961    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    // Composite primary key
967    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    // Indexes
973    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
1007// ============================================================================
1008// MongoDB Introspection
1009// ============================================================================
1010
1011/// MongoDB collection introspection.
1012pub mod mongodb {
1013    use serde_json::Value as JsonValue;
1014
1015    use super::{ColumnInfo, NormalizedType, TableInfo};
1016
1017    /// Infer schema from MongoDB documents.
1018    #[derive(Debug, Clone, Default)]
1019    pub struct SchemaInferrer {
1020        /// Field types discovered.
1021        pub fields: std::collections::HashMap<String, FieldSchema>,
1022        /// Sample size.
1023        pub samples: usize,
1024    }
1025
1026    /// Inferred field schema.
1027    #[derive(Debug, Clone, Default)]
1028    pub struct FieldSchema {
1029        /// Field name.
1030        pub name: String,
1031        /// Types observed.
1032        pub types: Vec<String>,
1033        /// Whether field is always present.
1034        pub required: bool,
1035        /// Nested fields (for objects).
1036        pub nested: Option<Box<SchemaInferrer>>,
1037        /// Array element type.
1038        pub array_type: Option<String>,
1039    }
1040
1041    impl SchemaInferrer {
1042        /// Create a new inferrer.
1043        pub fn new() -> Self {
1044            Self::default()
1045        }
1046
1047        /// Add a document sample.
1048        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                    // Try to detect special types
1073                    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                    // Recurse for nested objects
1096                    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        /// Convert to TableInfo.
1109        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        // Pick most specific type
1135        if field.types.contains(&"objectId".to_string()) {
1136            NormalizedType::String // ObjectId maps to String
1137        } 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    /// Generate MongoDB collection indexes command.
1163    pub fn list_indexes_command(collection: &str) -> JsonValue {
1164        serde_json::json!({
1165            "listIndexes": collection
1166        })
1167    }
1168
1169    /// Generate MongoDB list collections command.
1170    pub fn list_collections_command() -> JsonValue {
1171        serde_json::json!({
1172            "listCollections": 1
1173        })
1174    }
1175
1176    /// Simple ISO datetime detection without chrono dependency.
1177    fn is_iso_datetime(s: &str) -> bool {
1178        // Check for ISO 8601 format: YYYY-MM-DDTHH:MM:SS or similar
1179        if s.len() < 10 {
1180            return false;
1181        }
1182
1183        let bytes = s.as_bytes();
1184        // Check YYYY-MM-DD pattern
1185        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
1193// ============================================================================
1194// Helpers
1195// ============================================================================
1196
1197fn 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    // Simplify common default expressions
1220    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