1use deadpool_postgres::Pool;
3use fraiseql_error::{FraiseQLError, Result};
4use tokio_postgres::Row;
5
6use crate::{
7 DatabaseType,
8 introspector::{DatabaseIntrospector, RelationInfo, RelationKind},
9};
10
11pub struct PostgresIntrospector {
13 pool: Pool,
14}
15
16impl PostgresIntrospector {
17 #[must_use]
19 pub const fn new(pool: Pool) -> Self {
20 Self { pool }
21 }
22}
23
24impl DatabaseIntrospector for PostgresIntrospector {
25 async fn list_fact_tables(&self) -> Result<Vec<String>> {
26 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
27 message: format!("Failed to acquire connection: {e}"),
28 })?;
29
30 let query = r"
33 SELECT table_name
34 FROM information_schema.tables
35 WHERE table_schema = ANY(current_schemas(false))
36 AND table_type = 'BASE TABLE'
37 AND table_name LIKE 'tf_%'
38 ORDER BY table_name
39 ";
40
41 let rows: Vec<Row> =
42 client.query(query, &[]).await.map_err(|e| FraiseQLError::Database {
43 message: format!("Failed to list fact tables: {e}"),
44 sql_state: e.code().map(|c| c.code().to_string()),
45 })?;
46
47 let tables = rows
48 .into_iter()
49 .map(|row| {
50 let name: String = row.get(0);
51 name
52 })
53 .collect();
54
55 Ok(tables)
56 }
57
58 async fn get_columns(&self, table_name: &str) -> Result<Vec<(String, String, bool)>> {
59 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
60 message: format!("Failed to acquire connection: {e}"),
61 })?;
62
63 let rows: Vec<Row> = if let Some(dot) = table_name.find('.') {
67 let (schema, name) = (&table_name[..dot], &table_name[dot + 1..]);
68 client
69 .query(
70 r"SELECT column_name, data_type, is_nullable = 'YES' as is_nullable
71 FROM information_schema.columns
72 WHERE table_name = $1 AND table_schema = $2
73 ORDER BY ordinal_position",
74 &[&name, &schema],
75 )
76 .await
77 } else {
78 client
79 .query(
80 r"SELECT column_name, data_type, is_nullable = 'YES' as is_nullable
81 FROM information_schema.columns
82 WHERE table_name = $1
83 AND table_schema = ANY(current_schemas(false))
84 ORDER BY ordinal_position",
85 &[&table_name],
86 )
87 .await
88 }
89 .map_err(|e| FraiseQLError::Database {
90 message: format!("Failed to query column information: {e}"),
91 sql_state: e.code().map(|c| c.code().to_string()),
92 })?;
93
94 let columns = rows
95 .into_iter()
96 .map(|row| {
97 let name: String = row.get(0);
98 let data_type: String = row.get(1);
99 let is_nullable: bool = row.get(2);
100 (name, data_type, is_nullable)
101 })
102 .collect();
103
104 Ok(columns)
105 }
106
107 async fn get_indexed_columns(&self, table_name: &str) -> Result<Vec<String>> {
108 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
109 message: format!("Failed to acquire connection: {e}"),
110 })?;
111
112 let rows: Vec<Row> = if let Some(dot) = table_name.find('.') {
116 let (schema, name) = (&table_name[..dot], &table_name[dot + 1..]);
117 client
118 .query(
119 r"SELECT DISTINCT a.attname AS column_name
120 FROM pg_index i
121 JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
122 JOIN pg_class t ON t.oid = i.indrelid
123 JOIN pg_namespace n ON n.oid = t.relnamespace
124 WHERE t.relname = $1 AND n.nspname = $2 AND a.attnum > 0
125 ORDER BY column_name",
126 &[&name, &schema],
127 )
128 .await
129 } else {
130 client
131 .query(
132 r"SELECT DISTINCT a.attname AS column_name
133 FROM pg_index i
134 JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
135 JOIN pg_class t ON t.oid = i.indrelid
136 JOIN pg_namespace n ON n.oid = t.relnamespace
137 WHERE t.relname = $1
138 AND n.nspname = ANY(current_schemas(false))
139 AND a.attnum > 0
140 ORDER BY column_name",
141 &[&table_name],
142 )
143 .await
144 }
145 .map_err(|e| FraiseQLError::Database {
146 message: format!("Failed to query index information: {e}"),
147 sql_state: e.code().map(|c| c.code().to_string()),
148 })?;
149
150 let indexed_columns = rows
151 .into_iter()
152 .map(|row| {
153 let name: String = row.get(0);
154 name
155 })
156 .collect();
157
158 Ok(indexed_columns)
159 }
160
161 fn database_type(&self) -> DatabaseType {
162 DatabaseType::PostgreSQL
163 }
164
165 async fn list_relations(&self) -> Result<Vec<RelationInfo>> {
166 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
167 message: format!("Failed to acquire connection: {e}"),
168 })?;
169
170 let rows: Vec<Row> = client
175 .query(
176 r"SELECT table_schema, table_name,
177 CASE table_type WHEN 'VIEW' THEN 'view' ELSE 'table' END AS kind
178 FROM information_schema.tables
179 WHERE table_schema = ANY(current_schemas(false))
180 ORDER BY table_schema, table_name",
181 &[],
182 )
183 .await
184 .map_err(|e| FraiseQLError::Database {
185 message: format!("Failed to list relations: {e}"),
186 sql_state: e.code().map(|c| c.code().to_string()),
187 })?;
188
189 let relations = rows
190 .into_iter()
191 .map(|row| {
192 let schema: String = row.get(0);
193 let name: String = row.get(1);
194 let kind_str: String = row.get(2);
195 let kind = if kind_str == "view" {
196 RelationKind::View
197 } else {
198 RelationKind::Table
199 };
200 RelationInfo { schema, name, kind }
201 })
202 .collect();
203
204 Ok(relations)
205 }
206
207 async fn get_sample_jsonb(
208 &self,
209 table_name: &str,
210 column_name: &str,
211 ) -> Result<Option<serde_json::Value>> {
212 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
213 message: format!("Failed to acquire connection: {e}"),
214 })?;
215
216 let query = format!(
219 r#"
220 SELECT "{column}"::text
221 FROM "{table}"
222 WHERE "{column}" IS NOT NULL
223 LIMIT 1
224 "#,
225 table = table_name,
226 column = column_name
227 );
228
229 let rows: Vec<Row> =
230 client.query(&query, &[]).await.map_err(|e| FraiseQLError::Database {
231 message: format!("Failed to query sample JSONB: {e}"),
232 sql_state: e.code().map(|c| c.code().to_string()),
233 })?;
234
235 if rows.is_empty() {
236 return Ok(None);
237 }
238
239 let json_text: Option<String> = rows[0].get(0);
240 if let Some(text) = json_text {
241 let value: serde_json::Value =
242 serde_json::from_str(&text).map_err(|e| FraiseQLError::Parse {
243 message: format!("Failed to parse JSONB sample: {e}"),
244 location: format!("{table_name}.{column_name}"),
245 })?;
246 Ok(Some(value))
247 } else {
248 Ok(None)
249 }
250 }
251
252 async fn function_exists(&self, schema: Option<&str>, name: &str) -> Result<Option<bool>> {
253 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
254 message: format!("Failed to acquire connection: {e}"),
255 })?;
256
257 let row = if let Some(schema) = schema {
263 client
264 .query_one(
265 "SELECT EXISTS(SELECT 1 FROM pg_proc p \
266 JOIN pg_namespace n ON n.oid = p.pronamespace \
267 WHERE n.nspname = $1 AND p.proname = $2 AND p.prokind IN ('f','p'))",
268 &[&schema, &name],
269 )
270 .await
271 } else {
272 client
273 .query_one(
274 "SELECT EXISTS(SELECT 1 FROM pg_proc p \
275 JOIN pg_namespace n ON n.oid = p.pronamespace \
276 WHERE p.proname = $1 AND n.nspname = ANY(current_schemas(false)) \
277 AND p.prokind IN ('f','p'))",
278 &[&name],
279 )
280 .await
281 }
282 .map_err(|e| FraiseQLError::Database {
283 message: format!("Failed to probe function existence: {e}"),
284 sql_state: e.code().map(|c| c.code().to_string()),
285 })?;
286
287 Ok(Some(row.get(0)))
288 }
289
290 async fn qualified_relation_exists(&self, schema: &str, name: &str) -> Result<Option<bool>> {
291 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
292 message: format!("Failed to acquire connection: {e}"),
293 })?;
294
295 let quoted = format!(
301 "{}.{}",
302 crate::identifier::quote_postgres_identifier(schema),
303 crate::identifier::quote_postgres_identifier(name),
304 );
305 let row = client
306 .query_one("SELECT to_regclass($1) IS NOT NULL", &["ed])
307 .await
308 .map_err(|e| FraiseQLError::Database {
309 message: format!("Failed to probe relation existence: {e}"),
310 sql_state: e.code().map(|c| c.code().to_string()),
311 })?;
312
313 Ok(Some(row.get(0)))
314 }
315}
316
317impl PostgresIntrospector {
318 pub async fn get_indexed_nested_columns(
356 &self,
357 view_name: &str,
358 ) -> Result<std::collections::HashSet<String>> {
359 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
360 message: format!("Failed to acquire connection: {e}"),
361 })?;
362
363 let query = r"
366 SELECT column_name
367 FROM information_schema.columns
368 WHERE table_name = $1
369 AND table_schema = ANY(current_schemas(false))
370 AND column_name LIKE '%__%'
371 ORDER BY column_name
372 ";
373
374 let rows: Vec<Row> =
375 client.query(query, &[&view_name]).await.map_err(|e| FraiseQLError::Database {
376 message: format!("Failed to query view columns: {e}"),
377 sql_state: e.code().map(|c| c.code().to_string()),
378 })?;
379
380 let indexed_columns: std::collections::HashSet<String> = rows
381 .into_iter()
382 .map(|row| {
383 let name: String = row.get(0);
384 name
385 })
386 .filter(|name| {
387 Self::is_indexed_column_name(name)
391 })
392 .collect();
393
394 Ok(indexed_columns)
395 }
396
397 fn is_indexed_column_name(name: &str) -> bool {
403 if !name.contains("__") {
405 return false;
406 }
407
408 if let Some(rest) = name.strip_prefix('f') {
410 if let Some(underscore_pos) = rest.find("__") {
411 let digits = &rest[..underscore_pos];
412 if digits.chars().all(|c| c.is_ascii_digit()) && !digits.is_empty() {
413 let field_part = &rest[underscore_pos + 2..];
415 if !field_part.is_empty()
416 && field_part.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
417 && !field_part.starts_with(|c: char| c.is_ascii_digit())
418 {
419 return true;
420 }
421 }
422 }
423 }
424
425 let segments: Vec<&str> = name.split("__").collect();
428 if segments.len() < 2 {
429 return false;
430 }
431
432 if segments[0] == "f" {
434 return false;
435 }
436
437 segments.iter().all(|s| {
440 !s.is_empty()
441 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
442 && !s.starts_with(|c: char| c.is_ascii_digit())
443 })
444 }
445
446 pub async fn get_view_columns(&self, view_name: &str) -> Result<Vec<String>> {
461 let client = self.pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
462 message: format!("Failed to acquire connection: {e}"),
463 })?;
464
465 let query = r"
466 SELECT column_name
467 FROM information_schema.columns
468 WHERE table_name = $1
469 AND table_schema = ANY(current_schemas(false))
470 ORDER BY ordinal_position
471 ";
472
473 let rows: Vec<Row> =
474 client.query(query, &[&view_name]).await.map_err(|e| FraiseQLError::Database {
475 message: format!("Failed to query view columns: {e}"),
476 sql_state: e.code().map(|c| c.code().to_string()),
477 })?;
478
479 let columns = rows
480 .into_iter()
481 .map(|row| {
482 let name: String = row.get(0);
483 name
484 })
485 .collect();
486
487 Ok(columns)
488 }
489}
490
491#[cfg(test)]
492mod tests;