1use std::sync::Arc;
4
5use async_trait::async_trait;
6use bytes::BufMut as _;
7use fraiseql_error::{FraiseQLError, Result};
8use tokio_postgres::Row;
9
10use super::{
11 PostgresAdapter, build_projection_select_sql, build_where_select_sql,
12 build_where_select_sql_ordered,
13};
14use crate::{
15 identifier::quote_postgres_identifier,
16 traits::{DatabaseAdapter, ProjectionRequest, SupportsMutations},
17 types::{
18 DatabaseType, JsonbValue, PoolMetrics, QueryParam,
19 sql_hints::{OrderByClause, SqlProjectionHint},
20 },
21 where_clause::WhereClause,
22};
23
24const PG_UNDEFINED_COLUMN: &str = "42703";
26
27#[derive(Debug)]
42enum FlexParam {
43 Null,
45 Text(String),
47}
48
49impl tokio_postgres::types::ToSql for FlexParam {
50 fn to_sql(
51 &self,
52 ty: &tokio_postgres::types::Type,
53 out: &mut bytes::BytesMut,
54 ) -> std::result::Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>>
55 {
56 use tokio_postgres::types::{IsNull, Type};
57 match self {
58 Self::Null => Ok(IsNull::Yes),
59 Self::Text(s) => {
60 if *ty == Type::JSONB {
61 out.put_u8(1);
63 out.extend_from_slice(s.as_bytes());
64 } else if *ty == Type::JSON {
65 out.extend_from_slice(s.as_bytes());
66 } else if *ty == Type::UUID {
67 let uuid = uuid::Uuid::parse_str(s)?;
68 out.extend_from_slice(uuid.as_bytes());
69 } else if *ty == Type::INT4 {
70 let n: i32 = s.parse()?;
71 out.put_i32(n);
72 } else if *ty == Type::INT8 {
73 let n: i64 = s.parse()?;
74 out.put_i64(n);
75 } else if *ty == Type::BOOL {
76 let b: bool = s.parse()?;
77 out.put_u8(u8::from(b));
78 } else {
79 out.extend_from_slice(s.as_bytes());
82 }
83 Ok(IsNull::No)
84 },
85 }
86 }
87
88 fn accepts(_ty: &tokio_postgres::types::Type) -> bool {
89 true
91 }
92
93 fn to_sql_checked(
94 &self,
95 ty: &tokio_postgres::types::Type,
96 out: &mut bytes::BytesMut,
97 ) -> std::result::Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>>
98 {
99 self.to_sql(ty, out)
102 }
103}
104
105fn enrich_undefined_column_error(
113 err: FraiseQLError,
114 view: &str,
115 where_clause: Option<&WhereClause>,
116) -> FraiseQLError {
117 let FraiseQLError::Database { ref sql_state, .. } = err else {
118 return err;
119 };
120 if sql_state.as_deref() != Some(PG_UNDEFINED_COLUMN) {
121 return err;
122 }
123 let native_cols: Vec<&str> =
124 where_clause.map(|wc| wc.native_column_names()).unwrap_or_default();
125 if native_cols.is_empty() {
126 return err;
127 }
128 FraiseQLError::Database {
129 message: format!(
130 "Column(s) {:?} referenced as native column(s) on `{view}` do not exist. \
131 These columns were auto-inferred from ID/UUID-typed query arguments. \
132 Either add the column(s) to the table/view, or set \
133 `native_columns = {{}}` explicitly in your schema to disable inference.",
134 native_cols,
135 ),
136 sql_state: Some(PG_UNDEFINED_COLUMN.to_string()),
137 }
138}
139
140fn row_to_map(row: &Row) -> std::collections::HashMap<String, serde_json::Value> {
145 let mut map = std::collections::HashMap::new();
146 for (idx, column) in row.columns().iter().enumerate() {
147 let column_name = column.name().to_string();
148 let value: serde_json::Value = if let Ok(v) = row.try_get::<_, i32>(idx) {
149 serde_json::json!(v)
150 } else if let Ok(v) = row.try_get::<_, i64>(idx) {
151 serde_json::json!(v)
152 } else if let Ok(v) = row.try_get::<_, f64>(idx) {
153 serde_json::json!(v)
154 } else if let Ok(v) = row.try_get::<_, String>(idx) {
155 serde_json::json!(v)
156 } else if let Ok(v) = row.try_get::<_, bool>(idx) {
157 serde_json::json!(v)
158 } else if let Ok(v) = row.try_get::<_, Vec<String>>(idx) {
159 serde_json::json!(v)
164 } else if let Ok(v) = row.try_get::<_, serde_json::Value>(idx) {
165 v
166 } else {
167 serde_json::Value::Null
168 };
169 map.insert(column_name, value);
170 }
171 map
172}
173
174pub(super) async fn apply_session_vars(
180 txn: &tokio_postgres::Transaction<'_>,
181 session_vars: &[(&str, &str)],
182) -> Result<()> {
183 for (name, value) in session_vars {
184 txn.execute("SELECT set_config($1, $2, true)", &[name, value])
185 .await
186 .map_err(|e| FraiseQLError::Database {
187 message: format!("set_config({name:?}) failed: {e}"),
188 sql_state: e.code().map(|c| c.code().to_string()),
189 })?;
190 }
191 Ok(())
192}
193
194#[async_trait]
198impl DatabaseAdapter for PostgresAdapter {
199 async fn execute_with_projection(
200 &self,
201 view: &str,
202 projection: Option<&SqlProjectionHint>,
203 where_clause: Option<&WhereClause>,
204 limit: Option<u32>,
205 offset: Option<u32>,
206 order_by: Option<&[OrderByClause]>,
207 ) -> Result<Vec<JsonbValue>> {
208 self.execute_with_projection_impl(view, projection, where_clause, limit, offset, order_by)
209 .await
210 }
211
212 async fn execute_where_query(
213 &self,
214 view: &str,
215 where_clause: Option<&WhereClause>,
216 limit: Option<u32>,
217 offset: Option<u32>,
218 order_by: Option<&[OrderByClause]>,
219 ) -> Result<Vec<JsonbValue>> {
220 let (sql, typed_params) =
221 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
222
223 let param_refs = crate::types::as_sql_param_refs(&typed_params);
224
225 self.execute_raw(&sql, ¶m_refs)
226 .await
227 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
228 }
229
230 async fn explain_where_query(
231 &self,
232 view: &str,
233 where_clause: Option<&WhereClause>,
234 limit: Option<u32>,
235 offset: Option<u32>,
236 ) -> Result<serde_json::Value> {
237 let (select_sql, typed_params) = build_where_select_sql(view, where_clause, limit, offset)?;
238 if select_sql.contains(';') {
241 return Err(FraiseQLError::Validation {
242 message: "EXPLAIN SQL must be a single statement".into(),
243 path: None,
244 });
245 }
246 let explain_sql = format!("EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) {select_sql}");
247
248 let param_refs = crate::types::as_sql_param_refs(&typed_params);
249
250 let client = self.acquire_connection_with_retry().await?;
251 let rows = client.query(explain_sql.as_str(), ¶m_refs).await.map_err(|e| {
252 FraiseQLError::Database {
253 message: format!("EXPLAIN ANALYZE failed: {e}"),
254 sql_state: e.code().map(|c| c.code().to_string()),
255 }
256 })?;
257
258 if let Some(row) = rows.first() {
259 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
260 message: format!("Failed to parse EXPLAIN output: {e}"),
261 sql_state: None,
262 })?;
263 Ok(plan)
264 } else {
265 Ok(serde_json::Value::Null)
266 }
267 }
268
269 fn database_type(&self) -> DatabaseType {
270 DatabaseType::PostgreSQL
271 }
272
273 async fn health_check(&self) -> Result<()> {
274 let client = self.acquire_connection_with_retry().await?;
276
277 client.query("SELECT 1", &[]).await.map_err(|e| FraiseQLError::Database {
278 message: format!("Health check failed: {e}"),
279 sql_state: e.code().map(|c| c.code().to_string()),
280 })?;
281
282 Ok(())
283 }
284
285 #[allow(clippy::cast_possible_truncation)] fn pool_metrics(&self) -> PoolMetrics {
287 let status = self.pool.status();
288
289 PoolMetrics {
290 total_connections: status.size as u32,
291 idle_connections: status.available as u32,
292 active_connections: (status.size - status.available) as u32,
293 waiting_requests: status.waiting as u32,
294 }
295 }
296
297 async fn execute_raw_query(
302 &self,
303 sql: &str,
304 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
305 let client = self.acquire_connection_with_retry().await?;
307
308 let rows: Vec<Row> = client.query(sql, &[]).await.map_err(|e| FraiseQLError::Database {
309 message: format!("Query execution failed: {e}"),
310 sql_state: e.code().map(|c| c.code().to_string()),
311 })?;
312
313 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
315 rows.iter().map(row_to_map).collect();
316
317 Ok(results)
318 }
319
320 async fn execute_parameterized_aggregate(
321 &self,
322 sql: &str,
323 params: &[serde_json::Value],
324 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
325 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
329 let param_refs = crate::types::as_sql_param_refs(&typed);
330
331 let client = self.acquire_connection_with_retry().await?;
332 let rows: Vec<Row> =
333 client.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
334 message: format!("Parameterized aggregate query failed: {e}"),
335 sql_state: e.code().map(|c| c.code().to_string()),
336 })?;
337
338 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
339 rows.iter().map(row_to_map).collect();
340
341 Ok(results)
342 }
343
344 async fn execute_parameterized_aggregate_with_session(
345 &self,
346 sql: &str,
347 params: &[serde_json::Value],
348 session_vars: &[(&str, &str)],
349 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
350 if session_vars.is_empty() {
351 return self.execute_parameterized_aggregate(sql, params).await;
352 }
353
354 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
355 let param_refs = crate::types::as_sql_param_refs(&typed);
356
357 let mut client = self.acquire_connection_with_retry().await?;
358 let txn =
359 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
360 message: format!("Failed to start session-var transaction: {e}"),
361 sql_state: e.code().map(|c| c.code().to_string()),
362 })?;
363 apply_session_vars(&txn, session_vars).await?;
364 let rows: Vec<Row> =
365 txn.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
366 message: format!("Parameterized aggregate query failed: {e}"),
367 sql_state: e.code().map(|c| c.code().to_string()),
368 })?;
369 txn.commit().await.map_err(|e| FraiseQLError::Database {
370 message: format!("Failed to commit session-var transaction: {e}"),
371 sql_state: e.code().map(|c| c.code().to_string()),
372 })?;
373
374 Ok(rows.iter().map(row_to_map).collect())
375 }
376
377 async fn execute_function_call(
378 &self,
379 function_name: &str,
380 args: &[serde_json::Value],
381 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
382 let quoted_fn = quote_postgres_identifier(function_name);
388 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
389 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
390
391 let mut client = self.acquire_connection_with_retry().await?;
392
393 let flex_args: Vec<FlexParam> = args
401 .iter()
402 .map(|v| match v {
403 serde_json::Value::Null => FlexParam::Null,
404 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
405 _ => FlexParam::Text(v.to_string()),
406 })
407 .collect();
408 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
409 .iter()
410 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
411 .collect();
412
413 if self.mutation_timing_enabled {
414 let txn =
418 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
419 message: format!("Failed to start mutation timing transaction: {e}"),
420 sql_state: e.code().map(|c| c.code().to_string()),
421 })?;
422
423 txn.execute(
424 "SELECT set_config($1, clock_timestamp()::text, true)",
425 &[&self.timing_variable_name],
426 )
427 .await
428 .map_err(|e| FraiseQLError::Database {
429 message: format!("Failed to set mutation timing variable: {e}"),
430 sql_state: e.code().map(|c| c.code().to_string()),
431 })?;
432
433 let rows: Vec<Row> = txn.query(sql.as_str(), params.as_slice()).await.map_err(|e| {
434 let detail = e.as_db_error().map_or("", |d| d.message());
435 FraiseQLError::Database {
436 message: format!("Function call {function_name} failed: {e}: {detail}"),
437 sql_state: e.code().map(|c| c.code().to_string()),
438 }
439 })?;
440
441 txn.commit().await.map_err(|e| FraiseQLError::Database {
442 message: format!("Failed to commit mutation timing transaction: {e}"),
443 sql_state: e.code().map(|c| c.code().to_string()),
444 })?;
445
446 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
447 rows.iter().map(row_to_map).collect();
448
449 Ok(results)
450 } else {
451 let rows: Vec<Row> =
452 client.query(sql.as_str(), params.as_slice()).await.map_err(|e| {
453 let detail = e.as_db_error().map_or("", |d| d.message());
454 FraiseQLError::Database {
455 message: format!("Function call {function_name} failed: {e}: {detail}"),
456 sql_state: e.code().map(|c| c.code().to_string()),
457 }
458 })?;
459
460 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
461 rows.iter().map(row_to_map).collect();
462
463 Ok(results)
464 }
465 }
466
467 async fn execute_function_call_with_session(
473 &self,
474 function_name: &str,
475 args: &[serde_json::Value],
476 session_vars: &[(&str, &str)],
477 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
478 if session_vars.is_empty() && !self.mutation_timing_enabled {
482 return self.execute_function_call(function_name, args).await;
483 }
484
485 let quoted_fn = quote_postgres_identifier(function_name);
486 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
487 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
488
489 let flex_args: Vec<FlexParam> = args
491 .iter()
492 .map(|v| match v {
493 serde_json::Value::Null => FlexParam::Null,
494 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
495 _ => FlexParam::Text(v.to_string()),
496 })
497 .collect();
498 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
499 .iter()
500 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
501 .collect();
502
503 let mut client = self.acquire_connection_with_retry().await?;
504 let txn =
505 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
506 message: format!("Failed to start session-var transaction: {e}"),
507 sql_state: e.code().map(|c| c.code().to_string()),
508 })?;
509
510 apply_session_vars(&txn, session_vars).await?;
512
513 if self.mutation_timing_enabled {
515 txn.execute(
516 "SELECT set_config($1, clock_timestamp()::text, true)",
517 &[&self.timing_variable_name],
518 )
519 .await
520 .map_err(|e| FraiseQLError::Database {
521 message: format!("Failed to set mutation timing variable: {e}"),
522 sql_state: e.code().map(|c| c.code().to_string()),
523 })?;
524 }
525
526 let rows: Vec<Row> = txn.query(sql.as_str(), params.as_slice()).await.map_err(|e| {
527 let detail = e.as_db_error().map_or("", |d| d.message());
528 FraiseQLError::Database {
529 message: format!("Function call {function_name} failed: {e}: {detail}"),
530 sql_state: e.code().map(|c| c.code().to_string()),
531 }
532 })?;
533
534 txn.commit().await.map_err(|e| FraiseQLError::Database {
535 message: format!("Failed to commit session-var transaction: {e}"),
536 sql_state: e.code().map(|c| c.code().to_string()),
537 })?;
538
539 Ok(rows.iter().map(row_to_map).collect())
540 }
541
542 async fn execute_where_query_arc_with_session(
543 &self,
544 view: &str,
545 where_clause: Option<&WhereClause>,
546 limit: Option<u32>,
547 offset: Option<u32>,
548 order_by: Option<&[OrderByClause]>,
549 session_vars: &[(&str, &str)],
550 ) -> Result<Arc<Vec<JsonbValue>>> {
551 if session_vars.is_empty() {
552 return self.execute_where_query_arc(view, where_clause, limit, offset, order_by).await;
553 }
554
555 let (sql, typed_params) =
556 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
557 let param_refs = crate::types::as_sql_param_refs(&typed_params);
558
559 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
560 .await
561 .map(Arc::new)
562 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
563 }
564
565 async fn execute_with_projection_arc_with_session(
566 &self,
567 request: &ProjectionRequest<'_>,
568 session_vars: &[(&str, &str)],
569 ) -> Result<Arc<Vec<JsonbValue>>> {
570 if session_vars.is_empty() {
571 return self.execute_with_projection_arc(request).await;
572 }
573
574 let Some(projection) = request.projection else {
577 return self
578 .execute_where_query_arc_with_session(
579 request.view,
580 request.where_clause,
581 request.limit,
582 request.offset,
583 request.order_by,
584 session_vars,
585 )
586 .await;
587 };
588
589 let (sql, typed_params) = build_projection_select_sql(
590 projection,
591 request.view,
592 request.where_clause,
593 request.limit,
594 request.offset,
595 request.order_by,
596 )?;
597 let param_refs = crate::types::as_sql_param_refs(&typed_params);
598
599 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
600 .await
601 .map(Arc::new)
602 }
603
604 async fn explain_query(
605 &self,
606 sql: &str,
607 _params: &[serde_json::Value],
608 ) -> Result<serde_json::Value> {
609 if sql.contains(';') {
613 return Err(FraiseQLError::Validation {
614 message: "EXPLAIN SQL must be a single statement".into(),
615 path: None,
616 });
617 }
618 let explain_sql = format!("EXPLAIN (ANALYZE false, FORMAT JSON) {sql}");
619 let client = self.acquire_connection_with_retry().await?;
620 let rows: Vec<Row> =
621 client
622 .query(explain_sql.as_str(), &[])
623 .await
624 .map_err(|e| FraiseQLError::Database {
625 message: format!("EXPLAIN failed: {e}"),
626 sql_state: e.code().map(|c| c.code().to_string()),
627 })?;
628
629 if let Some(row) = rows.first() {
630 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
631 message: format!("Failed to parse EXPLAIN output: {e}"),
632 sql_state: None,
633 })?;
634 Ok(plan)
635 } else {
636 Ok(serde_json::Value::Null)
637 }
638 }
639
640 async fn query_stats(&self, limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
641 self.pg_query_stats(limit).await
642 }
643
644 async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
645 self.pg_query_stats_by_id(id).await
646 }
647
648 async fn reset_query_stats(&self) -> Result<()> {
649 self.pg_reset_query_stats().await
650 }
651}
652
653impl SupportsMutations for PostgresAdapter {}