1use crate::error::QailError;
6use sqlx::postgres::PgPool;
7use sqlx::Row;
8
9#[derive(Debug, Clone)]
11pub struct ColumnInfo {
12 pub name: String,
13 pub data_type: String,
14 pub is_nullable: bool,
15 pub column_default: Option<String>,
16}
17
18pub async fn get_table_schema(pool: &PgPool, table_name: &str) -> Result<Vec<ColumnInfo>, QailError> {
20 let rows = sqlx::query(
21 r#"
22 SELECT
23 column_name,
24 data_type,
25 is_nullable,
26 column_default
27 FROM information_schema.columns
28 WHERE table_name = $1
29 ORDER BY ordinal_position
30 "#,
31 )
32 .bind(table_name)
33 .fetch_all(pool)
34 .await
35 .map_err(|e| QailError::Execution(e.to_string()))?;
36
37 let columns: Vec<ColumnInfo> = rows
38 .iter()
39 .map(|row| ColumnInfo {
40 name: row.get::<String, _>("column_name"),
41 data_type: row.get::<String, _>("data_type"),
42 is_nullable: row.get::<String, _>("is_nullable") == "YES",
43 column_default: row.try_get::<String, _>("column_default").ok(),
44 })
45 .collect();
46
47 if columns.is_empty() {
48 return Err(QailError::Execution(format!(
49 "Table '{}' not found or has no columns",
50 table_name
51 )));
52 }
53
54 Ok(columns)
55}
56
57fn pg_to_rust_type(pg_type: &str, nullable: bool) -> String {
59 let base_type = match pg_type.to_lowercase().as_str() {
60 "uuid" => "Uuid",
61 "text" | "varchar" | "character varying" | "char" | "character" | "name" => "String",
62 "int2" | "smallint" => "i16",
63 "int4" | "integer" | "int" => "i32",
64 "int8" | "bigint" => "i64",
65 "float4" | "real" => "f32",
66 "float8" | "double precision" => "f64",
67 "numeric" | "decimal" => "rust_decimal::Decimal",
68 "bool" | "boolean" => "bool",
69 "timestamp without time zone" | "timestamp" => "chrono::NaiveDateTime",
70 "timestamp with time zone" | "timestamptz" => "chrono::DateTime<chrono::Utc>",
71 "date" => "chrono::NaiveDate",
72 "time" | "time without time zone" => "chrono::NaiveTime",
73 "jsonb" | "json" => "serde_json::Value",
74 "bytea" => "Vec<u8>",
75 t if t.ends_with("[]") => {
76 let inner = pg_to_rust_type(&t[..t.len() - 2], false);
77 return format!("Vec<{}>", inner);
78 }
79 _ => "String", };
81
82 if nullable {
83 format!("Option<{}>", base_type)
84 } else {
85 base_type.to_string()
86 }
87}
88
89fn to_pascal_case(s: &str) -> String {
91 s.split('_')
92 .map(|word| {
93 let mut chars = word.chars();
94 match chars.next() {
95 None => String::new(),
96 Some(first) => first.to_uppercase().chain(chars).collect(),
97 }
98 })
99 .collect()
100}
101
102pub fn generate_struct(table_name: &str, columns: &[ColumnInfo]) -> String {
104 let struct_name = to_pascal_case(table_name);
105
106 let mut output = String::new();
107
108 output.push_str("use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};\n");
110 output.push_str("use serde::{Deserialize, Serialize};\n");
111 output.push_str("use sqlx::FromRow;\n");
112 output.push_str("use uuid::Uuid;\n");
113 output.push_str("\n");
114 output.push_str("#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]\n");
115 output.push_str(&format!("pub struct {} {{\n", struct_name));
116
117 for col in columns {
118 let rust_type = pg_to_rust_type(&col.data_type, col.is_nullable);
119 output.push_str(&format!(" pub {}: {},\n", col.name, rust_type));
120 }
121
122 output.push_str("}\n");
123
124 output
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_to_pascal_case() {
133 assert_eq!(to_pascal_case("users"), "Users");
134 assert_eq!(to_pascal_case("ai_knowledge_base"), "AiKnowledgeBase");
135 assert_eq!(to_pascal_case("order_items"), "OrderItems");
136 }
137
138 #[test]
139 fn test_pg_to_rust_type() {
140 assert_eq!(pg_to_rust_type("uuid", false), "Uuid");
141 assert_eq!(pg_to_rust_type("text", false), "String");
142 assert_eq!(pg_to_rust_type("integer", true), "Option<i32>");
143 assert_eq!(pg_to_rust_type("timestamp with time zone", false), "chrono::DateTime<chrono::Utc>");
144 assert_eq!(pg_to_rust_type("text[]", false), "Vec<String>");
145 }
146}