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 if *value == crate::changelog::CLOCK_TIMESTAMP_DIRECTIVE {
189 txn.execute("SELECT set_config($1, clock_timestamp()::text, true)", &[name])
190 .await
191 .map_err(|e| FraiseQLError::Database {
192 message: format!("set_config({name:?}, clock_timestamp()) failed: {e}"),
193 sql_state: e.code().map(|c| c.code().to_string()),
194 })?;
195 } else {
196 txn.execute("SELECT set_config($1, $2, true)", &[name, value])
197 .await
198 .map_err(|e| FraiseQLError::Database {
199 message: format!("set_config({name:?}) failed: {e}"),
200 sql_state: e.code().map(|c| c.code().to_string()),
201 })?;
202 }
203 }
204 Ok(())
205}
206
207pub(super) fn build_changelog_cte_sql(quoted_fn: &str, n_args: usize) -> String {
236 let placeholders: Vec<String> = (1..=n_args).map(|i| format!("${i}")).collect();
237 let object_type_idx = n_args + 1;
238 let modification_type_idx = n_args + 2;
239 let tenant_id_idx = n_args + 3;
240 let trace_id_idx = n_args + 4;
241 let schema_version_idx = n_args + 5;
242 let trace_context_idx = n_args + 6;
243 let started_var = crate::changelog::STARTED_AT_VAR;
244 let duration = crate::changelog::duration_ms_sql(started_var);
245 let calc_version = crate::changelog::DURATION_CALC_VERSION;
246 format!(
247 "WITH r AS MATERIALIZED (SELECT * FROM {quoted_fn}({args})), \
248 _changelog AS ( \
249 INSERT INTO core.tb_entity_change_log \
250 (object_type, modification_type, object_id, object_data, \
251 updated_fields, cascade, started_at, duration_ms, extra_metadata, \
252 tenant_id, trace_id, schema_version, trace_context, commit_time) \
253 SELECT \
254 COALESCE(r.entity_type, ${object_type_idx}), \
255 ${modification_type_idx}, \
256 r.entity_id, r.entity, r.updated_fields, r.cascade, \
257 current_setting('{started_var}')::timestamptz, \
258 {duration}, \
259 jsonb_build_object('duration_calc_version', {calc_version}::int), \
260 ${tenant_id_idx}::uuid, \
261 ${trace_id_idx}, \
262 ${schema_version_idx}, \
263 ${trace_context_idx}::jsonb, \
264 clock_timestamp() \
265 FROM r \
266 WHERE r.succeeded AND r.state_changed \
267 RETURNING 1 \
268 ) \
269 SELECT * FROM r",
270 args = placeholders.join(", "),
271 )
272}
273
274async fn prepare_cached_stmt(
286 client: &deadpool_postgres::Client,
287 sql: &str,
288) -> Result<tokio_postgres::Statement> {
289 client.prepare_cached(sql).await.map_err(|e| FraiseQLError::Database {
290 message: format!("Failed to prepare statement: {e}"),
291 sql_state: e.code().map(|c| c.code().to_string()),
292 })
293}
294
295#[async_trait]
299impl DatabaseAdapter for PostgresAdapter {
300 async fn execute_with_projection(
301 &self,
302 view: &str,
303 projection: Option<&SqlProjectionHint>,
304 where_clause: Option<&WhereClause>,
305 limit: Option<u32>,
306 offset: Option<u32>,
307 order_by: Option<&[OrderByClause]>,
308 ) -> Result<Vec<JsonbValue>> {
309 self.execute_with_projection_impl(view, projection, where_clause, limit, offset, order_by)
310 .await
311 }
312
313 async fn execute_where_query(
314 &self,
315 view: &str,
316 where_clause: Option<&WhereClause>,
317 limit: Option<u32>,
318 offset: Option<u32>,
319 order_by: Option<&[OrderByClause]>,
320 ) -> Result<Vec<JsonbValue>> {
321 let (sql, typed_params) =
322 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
323
324 let param_refs = crate::types::as_sql_param_refs(&typed_params);
325
326 self.execute_raw(&sql, ¶m_refs)
327 .await
328 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
329 }
330
331 async fn explain_where_query(
332 &self,
333 view: &str,
334 where_clause: Option<&WhereClause>,
335 limit: Option<u32>,
336 offset: Option<u32>,
337 ) -> Result<serde_json::Value> {
338 let (select_sql, typed_params) = build_where_select_sql(view, where_clause, limit, offset)?;
339 if select_sql.contains(';') {
342 return Err(FraiseQLError::Validation {
343 message: "EXPLAIN SQL must be a single statement".into(),
344 path: None,
345 });
346 }
347 let explain_sql = format!("EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) {select_sql}");
348
349 let param_refs = crate::types::as_sql_param_refs(&typed_params);
350
351 let client = self.acquire_connection_with_retry().await?;
352 let rows = client.query(explain_sql.as_str(), ¶m_refs).await.map_err(|e| {
353 FraiseQLError::Database {
354 message: format!("EXPLAIN ANALYZE failed: {e}"),
355 sql_state: e.code().map(|c| c.code().to_string()),
356 }
357 })?;
358
359 if let Some(row) = rows.first() {
360 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
361 message: format!("Failed to parse EXPLAIN output: {e}"),
362 sql_state: None,
363 })?;
364 Ok(plan)
365 } else {
366 Ok(serde_json::Value::Null)
367 }
368 }
369
370 fn database_type(&self) -> DatabaseType {
371 DatabaseType::PostgreSQL
372 }
373
374 async fn health_check(&self) -> Result<()> {
375 let client = self.acquire_connection_with_retry().await?;
377
378 client.query("SELECT 1", &[]).await.map_err(|e| FraiseQLError::Database {
379 message: format!("Health check failed: {e}"),
380 sql_state: e.code().map(|c| c.code().to_string()),
381 })?;
382
383 Ok(())
384 }
385
386 #[allow(clippy::cast_possible_truncation)] fn pool_metrics(&self) -> PoolMetrics {
388 let status = self.pool.status();
389
390 PoolMetrics {
391 total_connections: status.size as u32,
392 idle_connections: status.available as u32,
393 active_connections: (status.size - status.available) as u32,
394 waiting_requests: status.waiting as u32,
395 }
396 }
397
398 async fn execute_raw_query(
403 &self,
404 sql: &str,
405 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
406 let client = self.acquire_connection_with_retry().await?;
408
409 let rows: Vec<Row> = client.query(sql, &[]).await.map_err(|e| FraiseQLError::Database {
410 message: format!("Query execution failed: {e}"),
411 sql_state: e.code().map(|c| c.code().to_string()),
412 })?;
413
414 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
416 rows.iter().map(row_to_map).collect();
417
418 Ok(results)
419 }
420
421 async fn execute_parameterized_aggregate(
422 &self,
423 sql: &str,
424 params: &[serde_json::Value],
425 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
426 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
430 let param_refs = crate::types::as_sql_param_refs(&typed);
431
432 let client = self.acquire_connection_with_retry().await?;
433 let rows: Vec<Row> =
434 client.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
435 message: format!("Parameterized aggregate query failed: {e}"),
436 sql_state: e.code().map(|c| c.code().to_string()),
437 })?;
438
439 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
440 rows.iter().map(row_to_map).collect();
441
442 Ok(results)
443 }
444
445 async fn execute_parameterized_aggregate_with_session(
446 &self,
447 sql: &str,
448 params: &[serde_json::Value],
449 session_vars: &[(&str, &str)],
450 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
451 if session_vars.is_empty() {
452 return self.execute_parameterized_aggregate(sql, params).await;
453 }
454
455 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
456 let param_refs = crate::types::as_sql_param_refs(&typed);
457
458 let mut client = self.acquire_connection_with_retry().await?;
459 let txn =
460 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
461 message: format!("Failed to start session-var transaction: {e}"),
462 sql_state: e.code().map(|c| c.code().to_string()),
463 })?;
464 apply_session_vars(&txn, session_vars).await?;
465 let rows: Vec<Row> =
466 txn.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
467 message: format!("Parameterized aggregate query failed: {e}"),
468 sql_state: e.code().map(|c| c.code().to_string()),
469 })?;
470 txn.commit().await.map_err(|e| FraiseQLError::Database {
471 message: format!("Failed to commit session-var transaction: {e}"),
472 sql_state: e.code().map(|c| c.code().to_string()),
473 })?;
474
475 Ok(rows.iter().map(row_to_map).collect())
476 }
477
478 async fn execute_function_call(
479 &self,
480 function_name: &str,
481 args: &[serde_json::Value],
482 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
483 let quoted_fn = quote_postgres_identifier(function_name);
489 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
490 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
491
492 let mut client = self.acquire_connection_with_retry().await?;
493
494 let flex_args: Vec<FlexParam> = args
502 .iter()
503 .map(|v| match v {
504 serde_json::Value::Null => FlexParam::Null,
505 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
506 _ => FlexParam::Text(v.to_string()),
507 })
508 .collect();
509 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
510 .iter()
511 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
512 .collect();
513
514 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
518
519 if self.mutation_timing_enabled {
520 let txn =
524 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
525 message: format!("Failed to start mutation timing transaction: {e}"),
526 sql_state: e.code().map(|c| c.code().to_string()),
527 })?;
528
529 txn.execute(
530 "SELECT set_config($1, clock_timestamp()::text, true)",
531 &[&self.timing_variable_name],
532 )
533 .await
534 .map_err(|e| FraiseQLError::Database {
535 message: format!("Failed to set mutation timing variable: {e}"),
536 sql_state: e.code().map(|c| c.code().to_string()),
537 })?;
538
539 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
540 let detail = e.as_db_error().map_or("", |d| d.message());
541 FraiseQLError::Database {
542 message: format!("Function call {function_name} failed: {e}: {detail}"),
543 sql_state: e.code().map(|c| c.code().to_string()),
544 }
545 })?;
546
547 txn.commit().await.map_err(|e| FraiseQLError::Database {
548 message: format!("Failed to commit mutation timing transaction: {e}"),
549 sql_state: e.code().map(|c| c.code().to_string()),
550 })?;
551
552 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
553 rows.iter().map(row_to_map).collect();
554
555 Ok(results)
556 } else {
557 let rows: Vec<Row> = client.query(&stmt, params.as_slice()).await.map_err(|e| {
558 let detail = e.as_db_error().map_or("", |d| d.message());
559 FraiseQLError::Database {
560 message: format!("Function call {function_name} failed: {e}: {detail}"),
561 sql_state: e.code().map(|c| c.code().to_string()),
562 }
563 })?;
564
565 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
566 rows.iter().map(row_to_map).collect();
567
568 Ok(results)
569 }
570 }
571
572 async fn execute_function_call_with_session(
578 &self,
579 function_name: &str,
580 args: &[serde_json::Value],
581 session_vars: &[(&str, &str)],
582 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
583 if session_vars.is_empty() && !self.mutation_timing_enabled {
587 return self.execute_function_call(function_name, args).await;
588 }
589
590 let quoted_fn = quote_postgres_identifier(function_name);
591 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
592 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
593
594 let flex_args: Vec<FlexParam> = args
596 .iter()
597 .map(|v| match v {
598 serde_json::Value::Null => FlexParam::Null,
599 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
600 _ => FlexParam::Text(v.to_string()),
601 })
602 .collect();
603 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
604 .iter()
605 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
606 .collect();
607
608 let mut client = self.acquire_connection_with_retry().await?;
609 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
611 let txn =
612 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
613 message: format!("Failed to start session-var transaction: {e}"),
614 sql_state: e.code().map(|c| c.code().to_string()),
615 })?;
616
617 apply_session_vars(&txn, session_vars).await?;
619
620 if self.mutation_timing_enabled {
622 txn.execute(
623 "SELECT set_config($1, clock_timestamp()::text, true)",
624 &[&self.timing_variable_name],
625 )
626 .await
627 .map_err(|e| FraiseQLError::Database {
628 message: format!("Failed to set mutation timing variable: {e}"),
629 sql_state: e.code().map(|c| c.code().to_string()),
630 })?;
631 }
632
633 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
634 let detail = e.as_db_error().map_or("", |d| d.message());
635 FraiseQLError::Database {
636 message: format!("Function call {function_name} failed: {e}: {detail}"),
637 sql_state: e.code().map(|c| c.code().to_string()),
638 }
639 })?;
640
641 txn.commit().await.map_err(|e| FraiseQLError::Database {
642 message: format!("Failed to commit session-var transaction: {e}"),
643 sql_state: e.code().map(|c| c.code().to_string()),
644 })?;
645
646 Ok(rows.iter().map(row_to_map).collect())
647 }
648
649 async fn execute_function_call_with_changelog(
650 &self,
651 function_name: &str,
652 args: &[serde_json::Value],
653 session_vars: &[(&str, &str)],
654 changelog: Option<&crate::traits::ChangeLogWrite<'_>>,
655 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
656 let Some(changelog) = changelog else {
659 return self
660 .execute_function_call_with_session(function_name, args, session_vars)
661 .await;
662 };
663
664 let quoted_fn = quote_postgres_identifier(function_name);
665 let sql = build_changelog_cte_sql("ed_fn, args.len());
668
669 let mut flex_args: Vec<FlexParam> = args
676 .iter()
677 .map(|v| match v {
678 serde_json::Value::Null => FlexParam::Null,
679 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
680 _ => FlexParam::Text(v.to_string()),
681 })
682 .collect();
683 flex_args.push(FlexParam::Text(changelog.object_type.to_string()));
684 flex_args.push(FlexParam::Text(changelog.modification_type.to_string()));
685 flex_args
688 .push(changelog.tenant_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
689 flex_args
691 .push(changelog.trace_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
692 flex_args.push(
694 changelog
695 .schema_version
696 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
697 );
698 flex_args.push(
700 changelog
701 .trace_context
702 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
703 );
704 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
705 .iter()
706 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
707 .collect();
708
709 let mut client = self.acquire_connection_with_retry().await?;
710 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
713 let txn =
714 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
715 message: format!("Failed to start change-log outbox transaction: {e}"),
716 sql_state: e.code().map(|c| c.code().to_string()),
717 })?;
718
719 apply_session_vars(&txn, session_vars).await?;
722
723 if self.mutation_timing_enabled {
725 txn.execute(
726 "SELECT set_config($1, clock_timestamp()::text, true)",
727 &[&self.timing_variable_name],
728 )
729 .await
730 .map_err(|e| FraiseQLError::Database {
731 message: format!("Failed to set mutation timing variable: {e}"),
732 sql_state: e.code().map(|c| c.code().to_string()),
733 })?;
734 }
735
736 let started_at_set =
743 session_vars.iter().any(|(name, _)| *name == crate::changelog::STARTED_AT_VAR)
744 || (self.mutation_timing_enabled
745 && self.timing_variable_name == crate::changelog::STARTED_AT_VAR);
746 if !started_at_set {
747 txn.execute(
748 "SELECT set_config($1, clock_timestamp()::text, true)",
749 &[&crate::changelog::STARTED_AT_VAR],
750 )
751 .await
752 .map_err(|e| FraiseQLError::Database {
753 message: format!("Failed to stamp change-log started_at: {e}"),
754 sql_state: e.code().map(|c| c.code().to_string()),
755 })?;
756 }
757
758 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
759 let detail = e.as_db_error().map_or("", |d| d.message());
760 FraiseQLError::Database {
761 message: format!(
762 "Function call {function_name} (with change-log outbox) failed: {e}: {detail}"
763 ),
764 sql_state: e.code().map(|c| c.code().to_string()),
765 }
766 })?;
767
768 txn.commit().await.map_err(|e| FraiseQLError::Database {
769 message: format!("Failed to commit change-log outbox transaction: {e}"),
770 sql_state: e.code().map(|c| c.code().to_string()),
771 })?;
772
773 Ok(rows.iter().map(row_to_map).collect())
774 }
775
776 async fn execute_where_query_arc_with_session(
777 &self,
778 view: &str,
779 where_clause: Option<&WhereClause>,
780 limit: Option<u32>,
781 offset: Option<u32>,
782 order_by: Option<&[OrderByClause]>,
783 session_vars: &[(&str, &str)],
784 ) -> Result<Arc<Vec<JsonbValue>>> {
785 if session_vars.is_empty() {
786 return self.execute_where_query_arc(view, where_clause, limit, offset, order_by).await;
787 }
788
789 let (sql, typed_params) =
790 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
791 let param_refs = crate::types::as_sql_param_refs(&typed_params);
792
793 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
794 .await
795 .map(Arc::new)
796 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
797 }
798
799 async fn execute_with_projection_arc_with_session(
800 &self,
801 request: &ProjectionRequest<'_>,
802 session_vars: &[(&str, &str)],
803 ) -> Result<Arc<Vec<JsonbValue>>> {
804 if session_vars.is_empty() {
805 return self.execute_with_projection_arc(request).await;
806 }
807
808 let Some(projection) = request.projection else {
811 return self
812 .execute_where_query_arc_with_session(
813 request.view,
814 request.where_clause,
815 request.limit,
816 request.offset,
817 request.order_by,
818 session_vars,
819 )
820 .await;
821 };
822
823 let (sql, typed_params) = build_projection_select_sql(
824 projection,
825 request.view,
826 request.where_clause,
827 request.limit,
828 request.offset,
829 request.order_by,
830 )?;
831 let param_refs = crate::types::as_sql_param_refs(&typed_params);
832
833 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
834 .await
835 .map(Arc::new)
836 }
837
838 async fn explain_query(
839 &self,
840 sql: &str,
841 _params: &[serde_json::Value],
842 ) -> Result<serde_json::Value> {
843 if sql.contains(';') {
847 return Err(FraiseQLError::Validation {
848 message: "EXPLAIN SQL must be a single statement".into(),
849 path: None,
850 });
851 }
852 let explain_sql = format!("EXPLAIN (ANALYZE false, FORMAT JSON) {sql}");
853 let client = self.acquire_connection_with_retry().await?;
854 let rows: Vec<Row> =
855 client
856 .query(explain_sql.as_str(), &[])
857 .await
858 .map_err(|e| FraiseQLError::Database {
859 message: format!("EXPLAIN failed: {e}"),
860 sql_state: e.code().map(|c| c.code().to_string()),
861 })?;
862
863 if let Some(row) = rows.first() {
864 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
865 message: format!("Failed to parse EXPLAIN output: {e}"),
866 sql_state: None,
867 })?;
868 Ok(plan)
869 } else {
870 Ok(serde_json::Value::Null)
871 }
872 }
873
874 async fn query_stats(&self, limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
875 self.pg_query_stats(limit).await
876 }
877
878 async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
879 self.pg_query_stats_by_id(id).await
880 }
881
882 async fn reset_query_stats(&self) -> Result<()> {
883 self.pg_reset_query_stats().await
884 }
885}
886
887impl SupportsMutations for PostgresAdapter {}