Skip to main content

rust_ef_mysql/
introspection.rs

1//! Database introspection for MySQL via `information_schema`.
2
3use rust_ef::error::EFResult;
4use sqlx::Row;
5
6/// Column information from database introspection.
7#[derive(Debug, Clone)]
8pub struct DbColumn {
9    pub name: String,
10    pub data_type: String,
11    pub is_nullable: bool,
12    pub is_primary_key: bool,
13    pub max_length: Option<usize>,
14}
15
16/// Table information from database introspection.
17#[derive(Debug, Clone)]
18pub struct DbTable {
19    pub name: String,
20    pub columns: Vec<DbColumn>,
21}
22
23/// Reads tables and columns from a MySQL database.
24pub async fn introspect_mysql(connection_string: &str) -> EFResult<Vec<DbTable>> {
25    let pool = sqlx::MySqlPool::connect(connection_string)
26        .await
27        .map_err(|e| rust_ef::error::EFError::connection(format!("MySQL connect failed: {e}")))?;
28
29    let table_rows = sqlx::query(
30        "SELECT table_name FROM information_schema.tables \
31         WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE' \
32         AND table_name NOT LIKE '__ef_%' \
33         ORDER BY table_name",
34    )
35    .fetch_all(&pool)
36    .await
37    .map_err(|e| rust_ef::error::EFError::query(format!("Table query error: {e}")))?;
38
39    let mut tables = Vec::new();
40    for table_row in table_rows {
41        let table_name: String = table_row.try_get(0).map_err(map_row_err)?;
42
43        let col_rows = sqlx::query(
44            "SELECT c.column_name, c.data_type, c.is_nullable, c.character_maximum_length, \
45             CASE WHEN k.column_name IS NOT NULL THEN 1 ELSE 0 END AS is_pk \
46             FROM information_schema.columns c \
47             LEFT JOIN information_schema.key_column_usage k \
48               ON c.table_schema = k.table_schema \
49              AND c.table_name = k.table_name \
50              AND c.column_name = k.column_name \
51              AND k.constraint_name = 'PRIMARY' \
52             WHERE c.table_schema = DATABASE() AND c.table_name = ? \
53             ORDER BY c.ordinal_position",
54        )
55        .bind(&table_name)
56        .fetch_all(&pool)
57        .await
58        .map_err(|e| rust_ef::error::EFError::query(format!("Column query error: {e}")))?;
59
60        let mut columns = Vec::new();
61        for col_row in col_rows {
62            let col_name: String = col_row.try_get(0).map_err(map_row_err)?;
63            let data_type: String = col_row.try_get(1).map_err(map_row_err)?;
64            let is_nullable_str: String = col_row.try_get(2).map_err(map_row_err)?;
65            let max_length: Option<i64> = col_row.try_get(3).ok();
66            let is_pk: i32 = col_row.try_get(4).map_err(map_row_err)?;
67
68            columns.push(DbColumn {
69                name: col_name,
70                data_type,
71                is_nullable: is_nullable_str == "YES",
72                is_primary_key: is_pk != 0,
73                max_length: max_length.map(|n| n as usize),
74            });
75        }
76
77        tables.push(DbTable {
78            name: table_name,
79            columns,
80        });
81    }
82
83    Ok(tables)
84}
85
86fn map_row_err(e: sqlx::Error) -> rust_ef::error::EFError {
87    rust_ef::error::EFError::query(format!("Row read error: {e}"))
88}