1use deadpool_postgres::Pool;
3use fraiseql_error::{FraiseQLError, Result};
4use tokio_postgres::Row;
5
6use super::pg_detail;
7use crate::{
8 DatabaseType,
9 introspector::{DatabaseIntrospector, RelationInfo, RelationKind},
10};
11
12pub struct PostgresIntrospector {
14 pool: Pool,
15}
16
17fn split_relation(relation: &str) -> (Option<&str>, &str) {
20 relation
21 .find('.')
22 .map_or((None, relation), |dot| (Some(&relation[..dot]), &relation[dot + 1..]))
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct IndexInfo {
35 pub name: String,
37 pub unique: bool,
39 pub keys: Vec<String>,
43}
44
45impl PostgresIntrospector {
46 #[must_use]
48 pub const fn new(pool: Pool) -> Self {
49 Self { pool }
50 }
51}
52
53impl DatabaseIntrospector for PostgresIntrospector {
54 async fn list_fact_tables(&self) -> Result<Vec<String>> {
55 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
56 message: format!("Failed to acquire connection: {e}"),
57 })?;
58
59 let query = r"
62 SELECT table_name
63 FROM information_schema.tables
64 WHERE table_schema = ANY(current_schemas(false))
65 AND table_type = 'BASE TABLE'
66 AND table_name LIKE 'tf_%'
67 ORDER BY table_name
68 ";
69
70 let rows: Vec<Row> =
71 client.query(query, &[]).await.map_err(|e| FraiseQLError::Database {
72 message: format!("Failed to list fact tables: {}", pg_detail(&e)),
73 sql_state: e.code().map(|c| c.code().to_string()),
74 })?;
75
76 let tables = rows
77 .into_iter()
78 .map(|row| {
79 let name: String = row.get(0);
80 name
81 })
82 .collect();
83
84 Ok(tables)
85 }
86
87 async fn get_columns(&self, table_name: &str) -> Result<Vec<(String, String, bool)>> {
88 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
89 message: format!("Failed to acquire connection: {e}"),
90 })?;
91
92 let rows: Vec<Row> = if let Some(dot) = table_name.find('.') {
96 let (schema, name) = (&table_name[..dot], &table_name[dot + 1..]);
97 client
98 .query(
99 r"SELECT column_name,
100 -- An extension type (ltree, vector) reads as USER-DEFINED
101 -- here; its own name is in udt_name (#1498).
102 CASE WHEN data_type = 'USER-DEFINED' THEN udt_name ELSE data_type END,
103 is_nullable = 'YES' as is_nullable
104 FROM information_schema.columns
105 WHERE table_name = $1 AND table_schema = $2
106 ORDER BY ordinal_position",
107 &[&name, &schema],
108 )
109 .await
110 } else {
111 client
112 .query(
113 r"SELECT column_name,
114 -- An extension type (ltree, vector) reads as USER-DEFINED
115 -- here; its own name is in udt_name (#1498).
116 CASE WHEN data_type = 'USER-DEFINED' THEN udt_name ELSE data_type END,
117 is_nullable = 'YES' as is_nullable
118 FROM information_schema.columns
119 WHERE table_name = $1
120 AND table_schema = ANY(current_schemas(false))
121 ORDER BY ordinal_position",
122 &[&table_name],
123 )
124 .await
125 }
126 .map_err(|e| FraiseQLError::Database {
127 message: format!("Failed to query column information: {}", pg_detail(&e)),
128 sql_state: e.code().map(|c| c.code().to_string()),
129 })?;
130
131 let columns = rows
132 .into_iter()
133 .map(|row| {
134 let name: String = row.get(0);
135 let data_type: String = row.get(1);
136 let is_nullable: bool = row.get(2);
137 (name, data_type, is_nullable)
138 })
139 .collect();
140
141 Ok(columns)
142 }
143
144 async fn get_indexed_columns(&self, table_name: &str) -> Result<Vec<String>> {
145 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
146 message: format!("Failed to acquire connection: {e}"),
147 })?;
148
149 let rows: Vec<Row> = if let Some(dot) = table_name.find('.') {
153 let (schema, name) = (&table_name[..dot], &table_name[dot + 1..]);
154 client
155 .query(
156 r"SELECT DISTINCT a.attname AS column_name
157 FROM pg_index i
158 JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
159 JOIN pg_class t ON t.oid = i.indrelid
160 JOIN pg_namespace n ON n.oid = t.relnamespace
161 WHERE t.relname = $1 AND n.nspname = $2 AND a.attnum > 0
162 ORDER BY column_name",
163 &[&name, &schema],
164 )
165 .await
166 } else {
167 client
168 .query(
169 r"SELECT DISTINCT a.attname AS column_name
170 FROM pg_index i
171 JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
172 JOIN pg_class t ON t.oid = i.indrelid
173 JOIN pg_namespace n ON n.oid = t.relnamespace
174 WHERE t.relname = $1
175 AND n.nspname = ANY(current_schemas(false))
176 AND a.attnum > 0
177 ORDER BY column_name",
178 &[&table_name],
179 )
180 .await
181 }
182 .map_err(|e| FraiseQLError::Database {
183 message: format!("Failed to query index information: {}", pg_detail(&e)),
184 sql_state: e.code().map(|c| c.code().to_string()),
185 })?;
186
187 let indexed_columns = rows
188 .into_iter()
189 .map(|row| {
190 let name: String = row.get(0);
191 name
192 })
193 .collect();
194
195 Ok(indexed_columns)
196 }
197
198 fn database_type(&self) -> DatabaseType {
199 DatabaseType::PostgreSQL
200 }
201
202 async fn list_relations(&self) -> Result<Vec<RelationInfo>> {
203 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
204 message: format!("Failed to acquire connection: {e}"),
205 })?;
206
207 let rows: Vec<Row> = client
212 .query(
213 r"SELECT table_schema, table_name,
214 CASE table_type WHEN 'VIEW' THEN 'view' ELSE 'table' END AS kind
215 FROM information_schema.tables
216 WHERE table_schema = ANY(current_schemas(false))
217 ORDER BY table_schema, table_name",
218 &[],
219 )
220 .await
221 .map_err(|e| FraiseQLError::Database {
222 message: format!("Failed to list relations: {}", pg_detail(&e)),
223 sql_state: e.code().map(|c| c.code().to_string()),
224 })?;
225
226 let relations = rows
227 .into_iter()
228 .map(|row| {
229 let schema: String = row.get(0);
230 let name: String = row.get(1);
231 let kind_str: String = row.get(2);
232 let kind = if kind_str == "view" {
233 RelationKind::View
234 } else {
235 RelationKind::Table
236 };
237 RelationInfo { schema, name, kind }
238 })
239 .collect();
240
241 Ok(relations)
242 }
243
244 async fn get_sample_jsonb(
245 &self,
246 table_name: &str,
247 column_name: &str,
248 ) -> Result<Option<serde_json::Value>> {
249 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
250 message: format!("Failed to acquire connection: {e}"),
251 })?;
252
253 let query = format!(
256 r#"
257 SELECT "{column}"::text
258 FROM "{table}"
259 WHERE "{column}" IS NOT NULL
260 LIMIT 1
261 "#,
262 table = table_name,
263 column = column_name
264 );
265
266 let rows: Vec<Row> =
267 client.query(&query, &[]).await.map_err(|e| FraiseQLError::Database {
268 message: format!("Failed to query sample JSONB: {}", pg_detail(&e)),
269 sql_state: e.code().map(|c| c.code().to_string()),
270 })?;
271
272 if rows.is_empty() {
273 return Ok(None);
274 }
275
276 let json_text: Option<String> = rows[0].get(0);
277 if let Some(text) = json_text {
278 let value: serde_json::Value =
279 serde_json::from_str(&text).map_err(|e| FraiseQLError::Parse {
280 message: format!("Failed to parse JSONB sample: {e}"),
281 location: format!("{table_name}.{column_name}"),
282 })?;
283 Ok(Some(value))
284 } else {
285 Ok(None)
286 }
287 }
288
289 async fn get_sample_json_rows(
290 &self,
291 table_name: &str,
292 column_name: &str,
293 limit: usize,
294 ) -> Result<Vec<serde_json::Value>> {
295 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
296 message: format!("Failed to acquire connection: {e}"),
297 })?;
298
299 let table = table_name
303 .split('.')
304 .map(|part| format!("\"{}\"", part.replace('"', "\"\"")))
305 .collect::<Vec<_>>()
306 .join(".");
307 let column = format!("\"{}\"", column_name.replace('"', "\"\""));
308 let query =
309 format!("SELECT {column}::text FROM {table} WHERE {column} IS NOT NULL LIMIT {limit}");
310
311 let rows: Vec<Row> =
312 client.query(&query, &[]).await.map_err(|e| FraiseQLError::Database {
313 message: format!("Failed to query sample JSON rows: {}", pg_detail(&e)),
314 sql_state: e.code().map(|c| c.code().to_string()),
315 })?;
316
317 rows.iter()
318 .filter_map(|row| row.get::<_, Option<String>>(0))
319 .map(|text| {
320 serde_json::from_str(&text).map_err(|e| FraiseQLError::Parse {
321 message: format!("Failed to parse JSON sample: {e}"),
322 location: format!("{table_name}.{column_name}"),
323 })
324 })
325 .collect()
326 }
327
328 async fn function_exists(&self, schema: Option<&str>, name: &str) -> Result<Option<bool>> {
329 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
330 message: format!("Failed to acquire connection: {e}"),
331 })?;
332
333 let row = if let Some(schema) = schema {
339 client
340 .query_one(
341 "SELECT EXISTS(SELECT 1 FROM pg_proc p \
342 JOIN pg_namespace n ON n.oid = p.pronamespace \
343 WHERE n.nspname = $1 AND p.proname = $2 AND p.prokind IN ('f','p'))",
344 &[&schema, &name],
345 )
346 .await
347 } else {
348 client
349 .query_one(
350 "SELECT EXISTS(SELECT 1 FROM pg_proc p \
351 JOIN pg_namespace n ON n.oid = p.pronamespace \
352 WHERE p.proname = $1 AND n.nspname = ANY(current_schemas(false)) \
353 AND p.prokind IN ('f','p'))",
354 &[&name],
355 )
356 .await
357 }
358 .map_err(|e| FraiseQLError::Database {
359 message: format!("Failed to probe function existence: {}", pg_detail(&e)),
360 sql_state: e.code().map(|c| c.code().to_string()),
361 })?;
362
363 Ok(Some(row.get(0)))
364 }
365
366 async fn qualified_relation_exists(&self, schema: &str, name: &str) -> Result<Option<bool>> {
367 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
368 message: format!("Failed to acquire connection: {e}"),
369 })?;
370
371 let quoted = format!(
377 "{}.{}",
378 crate::identifier::quote_postgres_identifier(schema),
379 crate::identifier::quote_postgres_identifier(name),
380 );
381 let row = client
382 .query_one("SELECT to_regclass($1) IS NOT NULL", &["ed])
383 .await
384 .map_err(|e| FraiseQLError::Database {
385 message: format!("Failed to probe relation existence: {}", pg_detail(&e)),
386 sql_state: e.code().map(|c| c.code().to_string()),
387 })?;
388
389 Ok(Some(row.get(0)))
390 }
391
392 async fn get_view_definition(&self, relation: &str) -> Result<Option<String>> {
393 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
394 message: format!("Failed to acquire connection: {e}"),
395 })?;
396
397 let row = client
403 .query_one("SELECT pg_get_viewdef(to_regclass($1), true)", &[&relation])
404 .await
405 .map_err(|e| FraiseQLError::Database {
406 message: format!("Failed to read view definition: {}", pg_detail(&e)),
407 sql_state: e.code().map(|c| c.code().to_string()),
408 })?;
409
410 Ok(row.get(0))
411 }
412}
413impl PostgresIntrospector {
414 pub async fn get_index_definitions(&self, relation: &str) -> Result<Vec<IndexInfo>> {
430 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
431 message: format!("Failed to acquire connection: {e}"),
432 })?;
433
434 let (schema, name) = split_relation(relation);
435 let query = r"
436 SELECT i.relname AS index_name,
437 ix.indisunique AS is_unique,
438 array_agg(pg_get_indexdef(ix.indexrelid, k.ord::int, true)
439 ORDER BY k.ord) AS keys
440 FROM pg_index ix
441 JOIN pg_class i ON i.oid = ix.indexrelid
442 JOIN pg_class t ON t.oid = ix.indrelid
443 JOIN pg_namespace n ON n.oid = t.relnamespace
444 JOIN LATERAL generate_series(1, ix.indnkeyatts) AS k(ord) ON true
445 WHERE t.relname = $1
446 AND ($2::text IS NULL OR n.nspname = $2)
447 AND ($2::text IS NOT NULL OR n.nspname = ANY(current_schemas(false)))
448 GROUP BY i.relname, ix.indisunique
449 ORDER BY i.relname
450 ";
451
452 let rows: Vec<Row> =
453 client
454 .query(query, &[&name, &schema])
455 .await
456 .map_err(|e| FraiseQLError::Database {
457 message: format!("Failed to query index definitions: {}", pg_detail(&e)),
458 sql_state: e.code().map(|c| c.code().to_string()),
459 })?;
460
461 Ok(rows
462 .into_iter()
463 .map(|row| IndexInfo {
464 name: row.get(0),
465 unique: row.get(1),
466 keys: row.get::<_, Vec<String>>(2),
467 })
468 .collect())
469 }
470
471 pub async fn resolve_base_relations(&self, view: &str) -> Result<Vec<String>> {
487 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
488 message: format!("Failed to acquire connection: {e}"),
489 })?;
490
491 let (schema, name) = split_relation(view);
492 let query = r"
493 SELECT DISTINCT base.relname
494 FROM pg_class v
495 JOIN pg_namespace vn ON vn.oid = v.relnamespace
496 JOIN pg_rewrite r ON r.ev_class = v.oid
497 JOIN pg_depend d ON d.objid = r.oid
498 JOIN pg_class base ON base.oid = d.refobjid
499 WHERE v.relname = $1
500 AND ($2::text IS NULL OR vn.nspname = $2)
501 AND ($2::text IS NOT NULL OR vn.nspname = ANY(current_schemas(false)))
502 AND base.relkind IN ('r', 'm', 'p')
503 AND base.oid <> v.oid
504 ORDER BY base.relname
505 ";
506
507 let rows: Vec<Row> =
508 client
509 .query(query, &[&name, &schema])
510 .await
511 .map_err(|e| FraiseQLError::Database {
512 message: format!("Failed to resolve view base relations: {}", pg_detail(&e)),
513 sql_state: e.code().map(|c| c.code().to_string()),
514 })?;
515
516 Ok(rows.into_iter().map(|row| row.get(0)).collect())
517 }
518
519 pub async fn get_view_columns(&self, view_name: &str) -> Result<Vec<String>> {
534 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
535 message: format!("Failed to acquire connection: {e}"),
536 })?;
537
538 let query = r"
539 SELECT column_name
540 FROM information_schema.columns
541 WHERE table_name = $1
542 AND table_schema = ANY(current_schemas(false))
543 ORDER BY ordinal_position
544 ";
545
546 let rows: Vec<Row> =
547 client.query(query, &[&view_name]).await.map_err(|e| FraiseQLError::Database {
548 message: format!("Failed to query view columns: {}", pg_detail(&e)),
549 sql_state: e.code().map(|c| c.code().to_string()),
550 })?;
551
552 let columns = rows
553 .into_iter()
554 .map(|row| {
555 let name: String = row.get(0);
556 name
557 })
558 .collect();
559
560 Ok(columns)
561 }
562}