larust_db/sql/
introspect.rs1use larust_core::AppError;
12use larust_orm::Backend;
13use sqlx::any::AnyTypeInfoKind;
14use sqlx::Row;
15
16#[derive(Debug, Clone)]
26pub struct ColumnInfo {
27 pub name: String,
28 pub not_null: bool,
29 pub kind: AnyTypeInfoKind,
30}
31
32fn internal<E: std::error::Error + Send + Sync + 'static>(error: E) -> AppError {
33 AppError::Internal(Box::new(error))
34}
35
36pub async fn list_tables() -> Result<Vec<String>, AppError> {
41 larust_orm::table_names().await
42}
43
44pub async fn list_indexes(table: &str) -> Result<Vec<serde_json::Value>, AppError> {
51 let pool = larust_orm::pool()?;
52 let rows = match larust_orm::backend() {
53 Backend::Sqlite => {
54 let sql = format!("PRAGMA index_list(\"{table}\")");
55 sqlx::query(&sql).fetch_all(pool).await.map_err(internal)?
56 }
57 Backend::MySql => {
58 let sql = format!("SHOW INDEX FROM \"{table}\"");
62 sqlx::query(&sql).fetch_all(pool).await.map_err(internal)?
63 }
64 backend @ Backend::Postgres => {
65 let sql = format!(
66 "SELECT indexname, indexdef FROM pg_indexes WHERE tablename = {}",
67 larust_orm::placeholder(backend, 1)
68 );
69 sqlx::query(&sql)
70 .bind(table)
71 .fetch_all(pool)
72 .await
73 .map_err(internal)?
74 }
75 };
76 Ok(rows.iter().map(crate::sql::codec::row_to_json).collect())
77}
78
79pub async fn list_foreign_keys(table: &str) -> Result<Vec<serde_json::Value>, AppError> {
82 let pool = larust_orm::pool()?;
83 let rows = match larust_orm::backend() {
84 Backend::Sqlite => {
85 let sql = format!("PRAGMA foreign_key_list(\"{table}\")");
86 sqlx::query(&sql).fetch_all(pool).await.map_err(internal)?
87 }
88 Backend::MySql => {
89 let sql = "SELECT column_name, referenced_table_name, referenced_column_name, \
90 constraint_name FROM information_schema.key_column_usage \
91 WHERE table_schema = DATABASE() AND table_name = ? \
92 AND referenced_table_name IS NOT NULL";
93 sqlx::query(sql)
94 .bind(table)
95 .fetch_all(pool)
96 .await
97 .map_err(internal)?
98 }
99 Backend::Postgres => {
100 let sql = "SELECT kcu.column_name, ccu.table_name AS referenced_table_name, \
101 ccu.column_name AS referenced_column_name, tc.constraint_name \
102 FROM information_schema.table_constraints tc \
103 JOIN information_schema.key_column_usage kcu \
104 ON tc.constraint_name = kcu.constraint_name \
105 AND tc.table_schema = kcu.table_schema \
106 JOIN information_schema.constraint_column_usage ccu \
107 ON tc.constraint_name = ccu.constraint_name \
108 AND tc.table_schema = ccu.table_schema \
109 WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = $1";
110 sqlx::query(sql)
111 .bind(table)
112 .fetch_all(pool)
113 .await
114 .map_err(internal)?
115 }
116 };
117 Ok(rows.iter().map(crate::sql::codec::row_to_json).collect())
118}
119
120pub async fn table_columns(table: &str) -> Result<Vec<ColumnInfo>, AppError> {
126 let pool = larust_orm::pool()?;
127 match larust_orm::backend() {
128 Backend::Sqlite => {
129 let sql = format!("PRAGMA table_info(\"{table}\")");
130 let rows = sqlx::query(&sql).fetch_all(pool).await.map_err(internal)?;
131 Ok(rows
132 .iter()
133 .map(|row| {
134 let name: String = row.get("name");
135 let type_text: String = row.get("type");
136 let notnull: i64 = row.get("notnull");
137 ColumnInfo {
138 name,
139 not_null: notnull != 0,
140 kind: infer_kind(&type_text),
141 }
142 })
143 .collect())
144 }
145 backend @ (Backend::MySql | Backend::Postgres) => {
146 let sql = format!(
147 "SELECT column_name, data_type, is_nullable FROM information_schema.columns \
148 WHERE table_name = {} ORDER BY ordinal_position",
149 larust_orm::placeholder(backend, 1)
150 );
151 let rows = sqlx::query(&sql)
152 .bind(table)
153 .fetch_all(pool)
154 .await
155 .map_err(internal)?;
156 Ok(rows
157 .iter()
158 .map(|row| {
159 let name: String = row.get("column_name");
160 let type_text: String = row.get("data_type");
161 let is_nullable: String = row.get("is_nullable");
162 ColumnInfo {
163 name,
164 not_null: is_nullable != "YES",
165 kind: infer_kind(&type_text),
166 }
167 })
168 .collect())
169 }
170 }
171}
172
173pub async fn primary_key_columns(table: &str) -> Result<Vec<String>, AppError> {
176 let pool = larust_orm::pool()?;
177 match larust_orm::backend() {
178 Backend::Sqlite => {
179 let sql = format!("PRAGMA table_info(\"{table}\")");
183 let rows = sqlx::query(&sql).fetch_all(pool).await.map_err(internal)?;
184 let mut pk: Vec<(i64, String)> = rows
185 .iter()
186 .filter_map(|row| {
187 let ordinal: i64 = row.get("pk");
188 if ordinal == 0 {
189 return None;
190 }
191 let name: String = row.get("name");
192 Some((ordinal, name))
193 })
194 .collect();
195 pk.sort_by_key(|(ordinal, _)| *ordinal);
196 Ok(pk.into_iter().map(|(_, name)| name).collect())
197 }
198 backend @ (Backend::MySql | Backend::Postgres) => {
199 let sql = format!(
200 "SELECT kcu.column_name FROM information_schema.key_column_usage kcu \
201 JOIN information_schema.table_constraints tc \
202 ON kcu.constraint_name = tc.constraint_name \
203 AND kcu.table_schema = tc.table_schema \
204 WHERE tc.constraint_type = 'PRIMARY KEY' AND kcu.table_name = {} \
205 ORDER BY kcu.ordinal_position",
206 larust_orm::placeholder(backend, 1)
207 );
208 let rows = sqlx::query(&sql)
209 .bind(table)
210 .fetch_all(pool)
211 .await
212 .map_err(internal)?;
213 Ok(rows
214 .iter()
215 .map(|row| row.get::<String, _>("column_name"))
216 .collect())
217 }
218 }
219}
220
221fn infer_kind(declared_type: &str) -> AnyTypeInfoKind {
230 let upper = declared_type.to_ascii_uppercase();
231 if upper.contains("BOOL") {
232 AnyTypeInfoKind::Bool
233 } else if upper.contains("BLOB") || upper.contains("BINARY") || upper.contains("BYTEA") {
234 AnyTypeInfoKind::Blob
235 } else if upper.contains("DOUBLE")
236 || upper.contains("REAL")
237 || upper.contains("FLOA")
238 || upper.contains("DEC")
239 || upper.contains("NUMERIC")
240 {
241 AnyTypeInfoKind::Double
242 } else if upper.contains("INT") {
243 AnyTypeInfoKind::BigInt
244 } else {
245 AnyTypeInfoKind::Text
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn infer_kind_recognizes_common_sqlite_and_information_schema_type_names() {
255 assert_eq!(infer_kind("INTEGER"), AnyTypeInfoKind::BigInt);
256 assert_eq!(infer_kind("integer"), AnyTypeInfoKind::BigInt);
257 assert_eq!(infer_kind("BIGINT"), AnyTypeInfoKind::BigInt);
258 assert_eq!(infer_kind("TEXT"), AnyTypeInfoKind::Text);
259 assert_eq!(infer_kind("character varying"), AnyTypeInfoKind::Text);
260 assert_eq!(infer_kind("VARCHAR(255)"), AnyTypeInfoKind::Text);
261 assert_eq!(infer_kind("REAL"), AnyTypeInfoKind::Double);
262 assert_eq!(infer_kind("DOUBLE PRECISION"), AnyTypeInfoKind::Double);
263 assert_eq!(infer_kind("NUMERIC(10,2)"), AnyTypeInfoKind::Double);
264 assert_eq!(infer_kind("BOOLEAN"), AnyTypeInfoKind::Bool);
265 assert_eq!(infer_kind("BLOB"), AnyTypeInfoKind::Blob);
266 assert_eq!(infer_kind("bytea"), AnyTypeInfoKind::Blob);
267 assert_eq!(infer_kind("some unknown type"), AnyTypeInfoKind::Text);
268 }
269}