data_goes 0.1.0-alpha.2.97

Biblioteca experimental para demonstração.
Documentation
use crate::map_schema::{Column, Table, Schema, Database, Root, Server};
use tokio::sync::Mutex;
pub use crate::config::ConfigClient;
use mssql_client::{Config, Ready};
use anyhow::{Ok, Result};

const MSSQL_SCHEMA: &str = include_str!("../querys/sql_schema.sql");
const MYSQL_SCHEMA: &str = include_str!("../querys/mysql_schema.sql");
const CLICKHOUSE_SCHEMA: &str = include_str!("../querys/clickhouse_schema.sql");

pub enum DbClient {
    SqlServer(mssql_client::Client<Ready>),
    MySql(mysql_async::Conn),
    ClickHouse(clickhouse::Client)
}
pub struct Generator{   
    client:Mutex<DbClient>,
    host:String,
}


impl Generator {
    pub async fn new<'a>(cfg: &ConfigClient<'a>)->Result<Self>{
        let conn_mssql =format!("Server={};User Id={};Password={};TrustServerCertificate=true", cfg.host, cfg.user, cfg.password);
        if let std::result::Result::Ok(config) = Config::from_connection_string(&conn_mssql){
            if let std::result::Result::Ok(client) = mssql_client::Client::connect(config).await{
                return Ok(Self{client: Mutex::new(DbClient::SqlServer(client)), host: cfg.host.to_string()});
            }
        }
        let conn_mysql=format!("mysql://{}:{}@{}:3306", cfg.user, cfg.password, cfg.host);
        if let std::result::Result::Ok(opts)= mysql_async::Opts::from_url(&conn_mysql){
            if let std::result::Result::Ok(client)=mysql_async::Conn::new(opts).await{
                return  Ok(Self { client: Mutex::new(DbClient::MySql(client)), host: cfg.host.to_string() });
            }
        }
        let ch_url = format!("http://{}:8123", cfg.host);
        let client_ch = clickhouse::Client::default()
            .with_url(&ch_url)
            .with_user(cfg.user.to_string())
            .with_password(cfg.password.to_string());
        if let std::result::Result::Ok(_) = client_ch.query("Select 1").fetch_one::<u8>().await{
            return  Ok(Self { client: Mutex::new(DbClient::ClickHouse(client_ch)), host: cfg.host.to_string() });
        }
        anyhow::bail!("Erro de autentificação")
    }

    pub async fn build_from_toml(toml_path: &str, output_path: &str) -> anyhow::Result<()> {
    let toml_str = std::fs::read_to_string(toml_path)?;
    let app_config: crate::config::AppConfig = toml::from_str(&toml_str)?;

    let mut merged_root = Root {
        servers: std::collections::BTreeMap::new(),
    };

    for server_cfg in app_config.servers {
        let cfg = crate::config::ConfigClient {
            host: std::borrow::Cow::Owned(server_cfg.host.clone()),
            user: std::borrow::Cow::Owned(server_cfg.user.clone()),
            password: std::borrow::Cow::Owned(server_cfg.password.clone()),
        };

        if let std::result::Result::Ok(generator) = Generator::new(&cfg).await {
            if let std::result::Result::Ok(partial_root) = generator.get_schema().await {
                merged_root.servers.extend(partial_root.servers);
            }
        }
    }

    let rust_code = Self::code_generator(&merged_root);
    std::fs::write(output_path, rust_code)?;
    Ok(())
}
    pub async fn get_schema(&self)->Result<Root>{
        let mut root = Root{
            servers:std::collections::BTreeMap::new(),
        };
        let mut client_lock = self.client.lock().await;
        match &mut *client_lock {
            DbClient::SqlServer(client)=>{
            let rows=client.query(MSSQL_SCHEMA, &[]).await?;
            for result in rows{
                let row= result?;
                Self::insert_node(&mut root, &self.host, row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?);
            }
        }, 
            DbClient::MySql(client)=>{
                use mysql_async::prelude::Queryable;
                let rows: Vec<(String, String, String, String, String)> = client.query(MYSQL_SCHEMA).await?;
                for (db, schema_name, table_name, col_name, col_type) in rows {
                    Self::insert_node(&mut root, &self.host, db, schema_name, table_name, col_name, col_type);
                }
            },
            DbClient::ClickHouse(client)=>{
                #[derive(clickhouse::Row, serde::Deserialize)]
                struct ColRow {
                    db: String,
                    schema_name: String,
                    table_name: String,
                    column_name: String,
                    data_type: String,
                }

                let mut cursor = client.query(CLICKHOUSE_SCHEMA).fetch::<ColRow>()?;
                while let Some(row) = cursor.next().await? {
                    Self::insert_node(&mut root, &self.host, row.db, row.schema_name, row.table_name, row.column_name, row.data_type);
                }
            }
        }
        let toml = toml::to_string_pretty(&root)?;
        std::fs::write("schema.toml", toml)?;
        Ok(root)
    }
    fn insert_node(root: &mut Root, host: &str, db: String, schema_name: String, table_name: String, col_name: String, col_type: String) {
        let column = Column { name: col_name, data_type: col_type };
        let server = root.servers.entry(host.to_string()).or_insert(Server { databases: std::collections::BTreeMap::new() });
        let database = server.databases.entry(db).or_insert(Database { schemas: std::collections::BTreeMap::new() });
        let schema = database.schemas.entry(schema_name).or_insert(Schema { tables: std::collections::BTreeMap::new() });
        let table = schema.tables.entry(table_name).or_insert(Table { columns: Vec::new() });
        table.columns.push(column);
    }
    pub fn code_generator( root: &Root)->String{
        let mut code = String::new();

        code.push_str(r#" // --- NÃO ALTERE ESTE DOCUMENTO MANUALMENTE ---
        use std::marker::PhantomData;

pub struct Column<T> {
    pub nome: &'static str,
    pub _tipo: PhantomData<T>,
}

pub trait ClientSqlExt {
        "#
        );
       
        let mut processed_fns = std::collections::HashSet::new();
        for (_server_name, server) in &root.servers{
            for (db_name, database) in &server.databases{
                for (schema_name, schema) in &database.schemas{
                    for table in schema.tables.keys(){
                        let full_name = format!("{}_{}_{}_{}", _server_name, db_name, schema_name, table);
                        let struct_name = Self::sanitize_struct_name(&full_name);
                        let fn_name = Self::sanitize_field_name(&full_name);

                        if processed_fns.insert(fn_name.clone()) {
                            code.push_str(&format!(
                                "    fn {}(&self) -> Table{}<'_> {{ Table{} {{ client: self }} }}\n",
                                fn_name, struct_name, struct_name
                                ));
                        }
                        }
                    }
                }
            }
        code.push_str("}\n\nimpl ClientSqlExt for crate::ClientSql {}\n\n");
        let mut generated_structs = std::collections::HashSet::new();
        for (_server_name, server) in &root.servers{
            for (db_name, database) in &server.databases{
                for (schema_name ,schema) in &database.schemas{
                    for(table_name, table ) in &schema.tables{
                        let full_name = format!("{}_{}_{}",db_name, schema_name, table_name);
                        let struct_name =Self::sanitize_struct_name(&full_name);
                        if generated_structs.insert(struct_name.clone()) {
                            let name_field = format!("{}Field", struct_name);
                            let name_table = format!("Table{}", struct_name);
                            code.push_str(&format!("pub struct Table{}<'a> {{\n    pub(crate) client: &'a crate::ClientSql,\n}}\n\n", struct_name));
                            code.push_str(&format!("impl<'a> {}<'a> {{\n", name_table));
                            code.push_str(&format!(
                                "    pub fn select<F, Retorno>(self, f: F)\n    where F: FnOnce({}) -> Retorno\n    {{\n",
                                name_field
                            ));
                            let mut column_seen = std::collections::HashSet::new();
                            for column in &table.columns{
                                let rust_type = Self::type_translator(&column.data_type);
                                let mut safe_col_name= Self::sanitize_field_name(&column.name);
                                let mut counter = 2;
                                let name_base = safe_col_name.clone();
                                while !column_seen.insert(safe_col_name.clone()) {
                                    safe_col_name = format!("{}_{}", name_base, counter);
                                    counter += 1;
                                }
                                code.push_str(&format!(" pub {}: Column<{}>,\n", safe_col_name, rust_type));
                            }
                            code.push_str("}\n\n");
                            code.push_str(&format!("pub struct {};\n\n", name_table));
                            code.push_str(&format!("impl {} {{\n", name_table));
                            code.push_str(&format!(
                                "    pub fn select<F, Retorno>(self, f: F)\n    where F: FnOnce({}) -> Retorno\n    {{\n",
                            name_field
                            ));
                            code.push_str(&format!("        let fields = {} {{\n", name_field));
                            
                        let mut columns_seen_impl = std::collections::HashSet::new();
                        for col in &table.columns {
                            let mut safe_col_name= Self::sanitize_field_name(&col.name);
                            let mut counter = 2;
                                let name_base = safe_col_name.clone();
                                while !columns_seen_impl.insert(safe_col_name.clone()) {
                                    safe_col_name = format!("{}_{}", name_base, counter);
                                    counter += 1;
                                }
                            code.push_str(&format!(
                                "            {}: Column {{ nome: \"{}\", _tipo: PhantomData }},\n",
                                safe_col_name, col.name
                            ));
                        }
                        code.push_str("        };\n");
                        code.push_str("        let _selecao = f(fields);\n");
                        code.push_str("    }\n");
                        code.push_str("}\n\n");
                        }
                    }
                }
            }
        }
        code
    }
    fn sanitize_struct_name(name: &str) -> String {
        let clean = Self::remove_accents(name);
        let mut result = String::new();
        let mut capitalize = true;
        for c in clean.chars() {
            if c.is_ascii_alphanumeric() {
                if capitalize {
                    result.push(c.to_ascii_uppercase());
                    capitalize = false;
                } else {
                    result.push(c.to_ascii_lowercase());
                }
            } else {
                capitalize = true;
            }
        }
        if result.is_empty() { return "TableObj".to_string(); }
        if result.starts_with(|c: char| c.is_ascii_digit()) { result.insert(0, '_'); }
        result
    }

    fn sanitize_field_name(name: &str) -> String {
        let clean = Self::remove_accents(name);
        let mut result = String::new();
        for c in clean.chars() {
            if c.is_ascii_alphanumeric() {
                result.push(c.to_ascii_lowercase());
            } else {
                if !result.ends_with('_') { result.push('_'); }
            }
        }
        let mut final_str = result.trim_matches('_').to_string();
        if final_str.is_empty() { final_str = "col".to_string(); } 
        else if final_str.starts_with(|c: char| c.is_ascii_digit()) { final_str.insert(0, '_'); }

        match final_str.as_str() {
            "type" | "match" | "let" | "final" | "fn" | "struct" | "enum" | "trait" | "impl" | "where" | "for" | "loop" | "while" | "if" | "else" | "return" | "break" | "continue" | "mut" | "ref" | "pub" | "use" | "mod" | "crate" | "self" | "super" | "as" | "in" | "move" | "const" | "static" | "true" | "false" => {
                final_str.push_str("_col");
            }
            _ => {}
        }
        final_str
    }

    fn remove_accents(name: &str) -> String {
        name.replace("á", "a").replace("ã", "a").replace("â", "a").replace("à", "a")
            .replace("é", "e").replace("ê", "e")
            .replace("í", "i")
            .replace("ó", "o").replace("õ", "o").replace("ô", "o")
            .replace("ú", "u")
            .replace("ç", "c")
            .replace("Á", "A").replace("Ã", "A").replace("Â", "A").replace("À", "A")
            .replace("É", "E").replace("Ê", "E")
            .replace("Í", "I")
            .replace("Ó", "O").replace("Õ", "O").replace("Ô", "O")
            .replace("Ú", "U")
            .replace("Ç", "C")
    }

    pub fn type_translator(sql_type: &str)->&'static str{
        let clean_type=sql_type.to_uppercase();
        let base = clean_type.split('(').next().unwrap_or("").trim();
        match base {
        "INT" | "INTEGER" => "Option<i32>",
        "BIGINT" => "Option<i64>",
        "SMALLINT" => "Option<i16>",
        "TINYINT" => "Option<u8>",
        "BIT" => "Option<bool>",
        "DECIMAL" | "NUMERIC" | "MONEY" => "Option<data_goes::rust_decimal::Decimal>",
        "FLOAT" => "Option<f64>",
        "REAL" => "Option<f32>",
        "CHAR" | "NCHAR" | "VARCHAR" | "NVARCHAR" | "TEXT" | "NTEXT" | "XML" => "Option<String>",
        "DATE" => "Option<data_goes::chrono::NaiveDate>",
        "TIME" => "Option<data_goes::chrono::NaiveTime>",
        "DATETIME" | "DATETIME2" | "SMALLDATETIME" => {
            "Option<data_goes::chrono::NaiveDateTime>"
        }
        "DATETIMEOFFSET" => {
            "Option<data_goes::chrono::DateTime<data_goes::chrono::FixedOffset>>"
        }
        "UNIQUEIDENTIFIER" => "Option<data_goes::uuid::Uuid>",
        "VARBINARY" | "IMAGE" | "TIMESTAMP" => "Option<Vec<u8>>",
        "Int8" => "Option<i8>",
        "Int16" => "Option<i16>",
        "Int32" => "Option<i32>",
        "Int64" => "Option<i64>",
        "UInt8" => "Option<u8>",
        "UInt16" => "Option<u16>",
        "UInt32" => "Option<u32>",
        "UInt64" => "Option<u64>",
        "Float32" => "Option<f32>",
        "Float64" => "Option<f64>",
        "Bool" => "Option<bool>",
        "String" => "Option<String>",
        "FixedString" => "Option<String>",
        "LowCardinality(String)" => "Option<String>",
        "LowCardinality(Nullable(String))" => "Option<String>",
        "Date" => "Option<data_goes::chrono::NaiveDate>",
        "Date32" => "Option<data_goes::chrono::NaiveDate>",
        "DateTime" => "Option<data_goes::chrono::NaiveDateTime>",
        "DateTime('America/Sao_Paulo')" => {
            "Option<data_goes::chrono::NaiveDateTime>"
        }
        "DateTime64(3)"
        | "DateTime64(6)"
        | "DateTime64(9)" => "Option<data_goes::chrono::NaiveDateTime>",
        "UUID" => "Option<data_goes::uuid::Uuid>",
        "IPv6" => "Option<std::net::Ipv6Addr>",
        "Decimal(15, 2)"
        | "Decimal(16, 1)"
        | "Decimal(16, 4)"
        | "Decimal(18, 2)"
        | "Decimal(38, 4)"
        | "Decimal(23, 16)" => "Option<data_goes::rust_decimal::Decimal>",
        "Nullable(String)" => "Option<String>",
        "Nullable(Int16)" => "Option<i16>",
        "Nullable(Int32)" => "Option<i32>",
        "Nullable(Int64)" => "Option<i64>",
        "Nullable(UInt8)" => "Option<u8>",
        "Nullable(UInt16)" => "Option<u16>",
        "Nullable(UInt32)" => "Option<u32>",
        "Nullable(UInt64)" => "Option<u64>",
        "Nullable(Float32)" => "Option<f32>",
        "Nullable(Float64)" => "Option<f64>",
        "Nullable(Date)" => "Option<data_goes::chrono::NaiveDate>",
        "Nullable(Date32)" => "Option<data_goes::chrono::NaiveDate>",
        "Nullable(DateTime)" => "Option<data_goes::chrono::NaiveDateTime>",
        "Nullable(DateTime64(3))"
        | "Nullable(DateTime64(6))" => {
            "Option<data_goes::chrono::NaiveDateTime>"
        }
        "Nullable(Decimal(15, 2))"
        | "Nullable(Decimal(16, 1))"
        | "Nullable(Decimal(16, 4))"
        | "Nullable(Decimal(38, 4))"
        | "Nullable(Decimal(23, 16))" => {
            "Option<data_goes::rust_decimal::Decimal>"
        }
        "Array(String)" => "Option<Vec<String>>",
        "Array(LowCardinality(String))" => "Option<Vec<String>>",
        "Array(UInt32)" => "Option<Vec<u32>>",
        "Array(UInt64)" => "Option<Vec<u64>>",
        "Array(Int32)" => "Option<Vec<i32>>",
        "Array(Int64)" => "Option<Vec<i64>>",
        "Array(Float64)" => "Option<Vec<f64>>",
        "Array(DateTime)" => "Option<Vec<data_goes::chrono::NaiveDateTime>>",
        "Array(DateTime64(9))" => {
            "Option<Vec<data_goes::chrono::NaiveDateTime>>"
        }
        "Map(String, String)" => {
            "Option<std::collections::HashMap<String, String>>"
        }
        "Map(String, UInt8)" => {
            "Option<std::collections::HashMap<String, u8>>"
        }
        "Map(String, UInt64)" => {
            "Option<std::collections::HashMap<String, u64>>"
        }
        "Map(LowCardinality(String), String)" => {
            "Option<std::collections::HashMap<String, String>>"
        }
        "Map(LowCardinality(String), UInt64)" => {
            "Option<std::collections::HashMap<String, u64>>"
        }
        "Map(LowCardinality(String), LowCardinality(String))" => {
            "Option<std::collections::HashMap<String, String>>"
        }
        "Tuple(UInt64, UInt64, UUID)" => {
            "Option<(u64, u64, data_goes::uuid::Uuid)>"
        }
        t if t.starts_with("Enum8(") => "Option<String>",
        t if t.starts_with("Enum16(") => "Option<String>",
        t if t.starts_with("Nullable(Enum8(") => "Option<String>",
        t if t.starts_with("Nullable(Enum16(") => "Option<String>",
        t if t.starts_with("Array(") => "Option<String>",
        t if t.starts_with("Map(") => "Option<String>",
        t if t.starts_with("Tuple(") => "Option<String>",
        t if t.starts_with("LowCardinality(") => "Option<String>",
        t if t.starts_with("Nullable(") => "Option<String>",

        _ => "Option<String>",
    }
    }
}