1use std::sync::Arc;
4
5use async_trait::async_trait;
6use bytes::BufMut as _;
7use fraiseql_error::{FraiseQLError, Result};
8use tokio_postgres::{
9 Row,
10 types::{FromSql, Kind, Type},
11};
12
13use super::{
14 PostgresAdapter, build_projection_select_sql, build_where_select_sql,
15 build_where_select_sql_ordered,
16};
17use crate::{
18 identifier::quote_postgres_identifier,
19 traits::{DatabaseAdapter, ProjectionRequest, SupportsMutations},
20 types::{
21 DatabaseType, JsonbValue, PoolMetrics, QueryParam,
22 sql_hints::{OrderByClause, SqlProjectionHint},
23 },
24 where_clause::WhereClause,
25};
26
27const PG_UNDEFINED_COLUMN: &str = "42703";
29
30#[derive(Debug)]
45enum FlexParam {
46 Null,
48 Text(String),
50}
51
52impl tokio_postgres::types::ToSql for FlexParam {
53 fn to_sql(
54 &self,
55 ty: &tokio_postgres::types::Type,
56 out: &mut bytes::BytesMut,
57 ) -> std::result::Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>>
58 {
59 use tokio_postgres::types::{IsNull, Type};
60 match self {
61 Self::Null => Ok(IsNull::Yes),
62 Self::Text(s) => {
63 if *ty == Type::JSONB {
64 out.put_u8(1);
66 out.extend_from_slice(s.as_bytes());
67 } else if *ty == Type::JSON {
68 out.extend_from_slice(s.as_bytes());
69 } else if *ty == Type::UUID {
70 let uuid = uuid::Uuid::parse_str(s)?;
71 out.extend_from_slice(uuid.as_bytes());
72 } else if *ty == Type::INT4 {
73 let n: i32 = s.parse()?;
74 out.put_i32(n);
75 } else if *ty == Type::INT8 {
76 let n: i64 = s.parse()?;
77 out.put_i64(n);
78 } else if *ty == Type::BOOL {
79 let b: bool = s.parse()?;
80 out.put_u8(u8::from(b));
81 } else {
82 out.extend_from_slice(s.as_bytes());
85 }
86 Ok(IsNull::No)
87 },
88 }
89 }
90
91 fn accepts(_ty: &tokio_postgres::types::Type) -> bool {
92 true
94 }
95
96 fn to_sql_checked(
97 &self,
98 ty: &tokio_postgres::types::Type,
99 out: &mut bytes::BytesMut,
100 ) -> std::result::Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>>
101 {
102 self.to_sql(ty, out)
105 }
106}
107
108fn enrich_undefined_column_error(
116 err: FraiseQLError,
117 view: &str,
118 where_clause: Option<&WhereClause>,
119) -> FraiseQLError {
120 let FraiseQLError::Database { ref sql_state, .. } = err else {
121 return err;
122 };
123 if sql_state.as_deref() != Some(PG_UNDEFINED_COLUMN) {
124 return err;
125 }
126 let native_cols: Vec<&str> =
127 where_clause.map(|wc| wc.native_column_names()).unwrap_or_default();
128 if native_cols.is_empty() {
129 return err;
130 }
131 FraiseQLError::Database {
132 message: format!(
133 "Column(s) {:?} referenced as native column(s) on `{view}` do not exist. \
134 These columns were auto-inferred from ID/UUID-typed query arguments. \
135 Either add the column(s) to the table/view, or set \
136 `native_columns = {{}}` explicitly in your schema to disable inference.",
137 native_cols,
138 ),
139 sql_state: Some(PG_UNDEFINED_COLUMN.to_string()),
140 }
141}
142
143struct PgEnumLabel(String);
150
151impl<'a> FromSql<'a> for PgEnumLabel {
152 fn from_sql(
153 _ty: &Type,
154 raw: &'a [u8],
155 ) -> std::result::Result<Self, Box<dyn std::error::Error + Sync + Send>> {
156 Ok(Self(std::str::from_utf8(raw)?.to_owned()))
157 }
158
159 fn accepts(ty: &Type) -> bool {
160 matches!(ty.kind(), Kind::Enum(_))
161 }
162}
163
164fn row_to_map(row: &Row) -> std::collections::HashMap<String, serde_json::Value> {
169 let mut map = std::collections::HashMap::new();
170 for (idx, column) in row.columns().iter().enumerate() {
171 let column_name = column.name().to_string();
172 let value: serde_json::Value = if let Ok(v) = row.try_get::<_, i16>(idx) {
173 serde_json::json!(v)
180 } else if let Ok(v) = row.try_get::<_, i32>(idx) {
181 serde_json::json!(v)
182 } else if let Ok(v) = row.try_get::<_, i64>(idx) {
183 serde_json::json!(v)
184 } else if let Ok(v) = row.try_get::<_, f64>(idx) {
185 serde_json::json!(v)
186 } else if let Ok(v) = row.try_get::<_, String>(idx) {
187 serde_json::json!(v)
188 } else if let Ok(v) = row.try_get::<_, bool>(idx) {
189 serde_json::json!(v)
190 } else if let Ok(v) = row.try_get::<_, rust_decimal::Decimal>(idx) {
191 let s = v.to_string();
198 serde_json::from_str::<serde_json::Value>(&s).unwrap_or(serde_json::Value::String(s))
199 } else if let Ok(v) = row.try_get::<_, uuid::Uuid>(idx) {
200 serde_json::json!(v.to_string())
203 } else if let Ok(v) = row.try_get::<_, chrono::DateTime<chrono::Utc>>(idx) {
204 serde_json::json!(v.to_rfc3339())
206 } else if let Ok(v) = row.try_get::<_, chrono::NaiveDateTime>(idx) {
207 serde_json::json!(v.to_string())
209 } else if let Ok(v) = row.try_get::<_, chrono::NaiveDate>(idx) {
210 serde_json::json!(v.to_string())
212 } else if let Ok(v) = row.try_get::<_, Vec<String>>(idx) {
213 serde_json::json!(v)
218 } else if let Ok(v) = row.try_get::<_, PgEnumLabel>(idx) {
219 serde_json::json!(v.0)
228 } else if let Ok(v) = row.try_get::<_, serde_json::Value>(idx) {
229 v
230 } else {
231 tracing::warn!(
235 column = %column_name,
236 pg_type = %column.type_(),
237 "row_to_map: column type not representable as JSON; returning null"
238 );
239 serde_json::Value::Null
240 };
241 map.insert(column_name, value);
242 }
243 map
244}
245
246pub(super) async fn apply_session_vars(
252 txn: &tokio_postgres::Transaction<'_>,
253 session_vars: &[(&str, &str)],
254) -> Result<()> {
255 for (name, value) in session_vars {
256 if *value == crate::changelog::CLOCK_TIMESTAMP_DIRECTIVE {
261 txn.execute("SELECT set_config($1, clock_timestamp()::text, true)", &[name])
262 .await
263 .map_err(|e| FraiseQLError::Database {
264 message: format!("set_config({name:?}, clock_timestamp()) failed: {e}"),
265 sql_state: e.code().map(|c| c.code().to_string()),
266 })?;
267 } else {
268 txn.execute("SELECT set_config($1, $2, true)", &[name, value])
269 .await
270 .map_err(|e| FraiseQLError::Database {
271 message: format!("set_config({name:?}) failed: {e}"),
272 sql_state: e.code().map(|c| c.code().to_string()),
273 })?;
274 }
275 }
276 Ok(())
277}
278
279pub(super) async fn mark_cdc_mediated(txn: &tokio_postgres::Transaction<'_>) -> Result<()> {
291 txn.execute(
292 "SELECT set_config($1, $2, true)",
293 &[
294 &crate::changelog::CDC_MEDIATED_VAR,
295 &crate::changelog::CDC_MEDIATED_ON,
296 ],
297 )
298 .await
299 .map_err(|e| FraiseQLError::Database {
300 message: format!("Failed to set {} marker: {e}", crate::changelog::CDC_MEDIATED_VAR),
301 sql_state: e.code().map(|c| c.code().to_string()),
302 })?;
303 Ok(())
304}
305
306pub(super) fn build_changelog_cte_sql(quoted_fn: &str, n_args: usize, pre_image: bool) -> String {
335 let placeholders: Vec<String> = (1..=n_args).map(|i| format!("${i}")).collect();
336 let object_type_idx = n_args + 1;
337 let modification_type_idx = n_args + 2;
338 let tenant_id_idx = n_args + 3;
339 let trace_id_idx = n_args + 4;
340 let schema_version_idx = n_args + 5;
341 let trace_context_idx = n_args + 6;
342 let actor_type_idx = n_args + 7;
343 let acting_for_idx = n_args + 8;
344 let started_var = crate::changelog::STARTED_AT_VAR;
345 let duration = crate::changelog::duration_ms_sql(started_var);
346 let calc_version = crate::changelog::DURATION_CALC_VERSION;
347 let after_before_cols = if pre_image {
356 "object_data, object_data_before"
357 } else {
358 "object_data"
359 };
360 let after_before_vals = if pre_image {
361 "r.entity, r.entity_before"
362 } else {
363 "r.entity"
364 };
365 format!(
366 "WITH r AS MATERIALIZED (SELECT * FROM {quoted_fn}({args})), \
367 _changelog AS ( \
368 INSERT INTO core.tb_entity_change_log \
369 (object_type, modification_type, object_id, {after_before_cols}, \
370 updated_fields, cascade, started_at, duration_ms, extra_metadata, \
371 tenant_id, trace_id, schema_version, trace_context, \
372 actor_type, acting_for, commit_time) \
373 SELECT \
374 COALESCE(r.entity_type, ${object_type_idx}), \
375 ${modification_type_idx}, \
376 r.entity_id, {after_before_vals}, r.updated_fields, r.cascade, \
377 current_setting('{started_var}')::timestamptz, \
378 {duration}, \
379 jsonb_build_object('duration_calc_version', {calc_version}::int), \
380 ${tenant_id_idx}::uuid, \
381 ${trace_id_idx}, \
382 ${schema_version_idx}, \
383 ${trace_context_idx}::jsonb, \
384 ${actor_type_idx}, \
385 ${acting_for_idx}::uuid, \
386 clock_timestamp() \
387 FROM r \
388 WHERE r.succeeded AND r.state_changed \
389 RETURNING 1 \
390 ) \
391 SELECT * FROM r",
392 args = placeholders.join(", "),
393 )
394}
395
396async fn prepare_cached_stmt(
408 client: &deadpool_postgres::Client,
409 sql: &str,
410) -> Result<tokio_postgres::Statement> {
411 client.prepare_cached(sql).await.map_err(|e| {
412 let detail = e.as_db_error().map_or_else(|| e.to_string(), |d| d.message().to_string());
420 FraiseQLError::Database {
421 message: format!("Failed to prepare statement: {detail}"),
422 sql_state: e.code().map(|c| c.code().to_string()),
423 }
424 })
425}
426
427#[async_trait]
431impl DatabaseAdapter for PostgresAdapter {
432 async fn execute_with_projection(
433 &self,
434 view: &str,
435 projection: Option<&SqlProjectionHint>,
436 where_clause: Option<&WhereClause>,
437 limit: Option<u32>,
438 offset: Option<u32>,
439 order_by: Option<&[OrderByClause]>,
440 ) -> Result<Vec<JsonbValue>> {
441 self.execute_with_projection_impl(view, projection, where_clause, limit, offset, order_by)
442 .await
443 }
444
445 async fn execute_where_query(
446 &self,
447 view: &str,
448 where_clause: Option<&WhereClause>,
449 limit: Option<u32>,
450 offset: Option<u32>,
451 order_by: Option<&[OrderByClause]>,
452 ) -> Result<Vec<JsonbValue>> {
453 let (sql, typed_params) =
454 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
455
456 let param_refs = crate::types::as_sql_param_refs(&typed_params);
457
458 self.execute_raw(&sql, ¶m_refs)
459 .await
460 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
461 }
462
463 async fn explain_where_query(
464 &self,
465 view: &str,
466 where_clause: Option<&WhereClause>,
467 limit: Option<u32>,
468 offset: Option<u32>,
469 ) -> Result<serde_json::Value> {
470 let (select_sql, typed_params) = build_where_select_sql(view, where_clause, limit, offset)?;
471 if select_sql.contains(';') {
474 return Err(FraiseQLError::Validation {
475 message: "EXPLAIN SQL must be a single statement".into(),
476 path: None,
477 });
478 }
479 let explain_sql = format!("EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) {select_sql}");
480
481 let param_refs = crate::types::as_sql_param_refs(&typed_params);
482
483 let client = self.acquire_connection_with_retry().await?;
484 let rows = client.query(explain_sql.as_str(), ¶m_refs).await.map_err(|e| {
485 FraiseQLError::Database {
486 message: format!("EXPLAIN ANALYZE failed: {e}"),
487 sql_state: e.code().map(|c| c.code().to_string()),
488 }
489 })?;
490
491 if let Some(row) = rows.first() {
492 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
493 message: format!("Failed to parse EXPLAIN output: {e}"),
494 sql_state: None,
495 })?;
496 Ok(plan)
497 } else {
498 Ok(serde_json::Value::Null)
499 }
500 }
501
502 fn database_type(&self) -> DatabaseType {
503 DatabaseType::PostgreSQL
504 }
505
506 async fn health_check(&self) -> Result<()> {
507 let client = self.acquire_connection_with_retry().await?;
509
510 client.query("SELECT 1", &[]).await.map_err(|e| FraiseQLError::Database {
511 message: format!("Health check failed: {e}"),
512 sql_state: e.code().map(|c| c.code().to_string()),
513 })?;
514
515 Ok(())
516 }
517
518 #[allow(clippy::cast_possible_truncation)] fn pool_metrics(&self) -> PoolMetrics {
520 let status = self.pool.status();
521
522 PoolMetrics {
523 total_connections: status.size as u32,
524 idle_connections: status.available as u32,
525 active_connections: (status.size - status.available) as u32,
526 waiting_requests: status.waiting as u32,
527 }
528 }
529
530 async fn execute_raw_query(
535 &self,
536 sql: &str,
537 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
538 let client = self.acquire_connection_with_retry().await?;
540
541 let rows: Vec<Row> = client.query(sql, &[]).await.map_err(|e| FraiseQLError::Database {
542 message: format!("Query execution failed: {e}"),
543 sql_state: e.code().map(|c| c.code().to_string()),
544 })?;
545
546 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
548 rows.iter().map(row_to_map).collect();
549
550 Ok(results)
551 }
552
553 async fn execute_parameterized_aggregate(
554 &self,
555 sql: &str,
556 params: &[serde_json::Value],
557 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
558 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
562 let param_refs = crate::types::as_sql_param_refs(&typed);
563
564 let client = self.acquire_connection_with_retry().await?;
565 let rows: Vec<Row> =
566 client.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
567 message: format!("Parameterized aggregate query failed: {e}"),
568 sql_state: e.code().map(|c| c.code().to_string()),
569 })?;
570
571 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
572 rows.iter().map(row_to_map).collect();
573
574 Ok(results)
575 }
576
577 async fn execute_parameterized_aggregate_with_session(
578 &self,
579 sql: &str,
580 params: &[serde_json::Value],
581 session_vars: &[(&str, &str)],
582 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
583 if session_vars.is_empty() {
584 return self.execute_parameterized_aggregate(sql, params).await;
585 }
586
587 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
588 let param_refs = crate::types::as_sql_param_refs(&typed);
589
590 let mut client = self.acquire_connection_with_retry().await?;
591 let txn =
592 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
593 message: format!("Failed to start session-var transaction: {e}"),
594 sql_state: e.code().map(|c| c.code().to_string()),
595 })?;
596 apply_session_vars(&txn, session_vars).await?;
597 let rows: Vec<Row> =
598 txn.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
599 message: format!("Parameterized aggregate query failed: {e}"),
600 sql_state: e.code().map(|c| c.code().to_string()),
601 })?;
602 txn.commit().await.map_err(|e| FraiseQLError::Database {
603 message: format!("Failed to commit session-var transaction: {e}"),
604 sql_state: e.code().map(|c| c.code().to_string()),
605 })?;
606
607 Ok(rows.iter().map(row_to_map).collect())
608 }
609
610 async fn execute_function_call(
611 &self,
612 function_name: &str,
613 args: &[serde_json::Value],
614 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
615 let quoted_fn = quote_postgres_identifier(function_name);
621 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
622 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
623
624 let mut client = self.acquire_connection_with_retry().await?;
625
626 let flex_args: Vec<FlexParam> = args
634 .iter()
635 .map(|v| match v {
636 serde_json::Value::Null => FlexParam::Null,
637 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
638 _ => FlexParam::Text(v.to_string()),
639 })
640 .collect();
641 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
642 .iter()
643 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
644 .collect();
645
646 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
650
651 if self.mutation_timing_enabled {
652 let txn =
656 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
657 message: format!("Failed to start mutation timing transaction: {e}"),
658 sql_state: e.code().map(|c| c.code().to_string()),
659 })?;
660
661 txn.execute(
662 "SELECT set_config($1, clock_timestamp()::text, true)",
663 &[&self.timing_variable_name],
664 )
665 .await
666 .map_err(|e| FraiseQLError::Database {
667 message: format!("Failed to set mutation timing variable: {e}"),
668 sql_state: e.code().map(|c| c.code().to_string()),
669 })?;
670
671 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
672 let detail = e.as_db_error().map_or("", |d| d.message());
673 FraiseQLError::Database {
674 message: format!("Function call {function_name} failed: {e}: {detail}"),
675 sql_state: e.code().map(|c| c.code().to_string()),
676 }
677 })?;
678
679 txn.commit().await.map_err(|e| FraiseQLError::Database {
680 message: format!("Failed to commit mutation timing transaction: {e}"),
681 sql_state: e.code().map(|c| c.code().to_string()),
682 })?;
683
684 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
685 rows.iter().map(row_to_map).collect();
686
687 Ok(results)
688 } else {
689 let rows: Vec<Row> = client.query(&stmt, params.as_slice()).await.map_err(|e| {
690 let detail = e.as_db_error().map_or("", |d| d.message());
691 FraiseQLError::Database {
692 message: format!("Function call {function_name} failed: {e}: {detail}"),
693 sql_state: e.code().map(|c| c.code().to_string()),
694 }
695 })?;
696
697 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
698 rows.iter().map(row_to_map).collect();
699
700 Ok(results)
701 }
702 }
703
704 async fn execute_function_call_with_session(
710 &self,
711 function_name: &str,
712 args: &[serde_json::Value],
713 session_vars: &[(&str, &str)],
714 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
715 if session_vars.is_empty() && !self.mutation_timing_enabled {
719 return self.execute_function_call(function_name, args).await;
720 }
721
722 let quoted_fn = quote_postgres_identifier(function_name);
723 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
724 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
725
726 let flex_args: Vec<FlexParam> = args
728 .iter()
729 .map(|v| match v {
730 serde_json::Value::Null => FlexParam::Null,
731 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
732 _ => FlexParam::Text(v.to_string()),
733 })
734 .collect();
735 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
736 .iter()
737 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
738 .collect();
739
740 let mut client = self.acquire_connection_with_retry().await?;
741 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
743 let txn =
744 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
745 message: format!("Failed to start session-var transaction: {e}"),
746 sql_state: e.code().map(|c| c.code().to_string()),
747 })?;
748
749 apply_session_vars(&txn, session_vars).await?;
751
752 mark_cdc_mediated(&txn).await?;
757
758 if self.mutation_timing_enabled {
760 txn.execute(
761 "SELECT set_config($1, clock_timestamp()::text, true)",
762 &[&self.timing_variable_name],
763 )
764 .await
765 .map_err(|e| FraiseQLError::Database {
766 message: format!("Failed to set mutation timing variable: {e}"),
767 sql_state: e.code().map(|c| c.code().to_string()),
768 })?;
769 }
770
771 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
772 let detail = e.as_db_error().map_or("", |d| d.message());
773 FraiseQLError::Database {
774 message: format!("Function call {function_name} failed: {e}: {detail}"),
775 sql_state: e.code().map(|c| c.code().to_string()),
776 }
777 })?;
778
779 txn.commit().await.map_err(|e| FraiseQLError::Database {
780 message: format!("Failed to commit session-var transaction: {e}"),
781 sql_state: e.code().map(|c| c.code().to_string()),
782 })?;
783
784 Ok(rows.iter().map(row_to_map).collect())
785 }
786
787 async fn execute_function_call_with_changelog(
788 &self,
789 function_name: &str,
790 args: &[serde_json::Value],
791 session_vars: &[(&str, &str)],
792 changelog: Option<&crate::traits::ChangeLogWrite<'_>>,
793 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
794 let Some(changelog) = changelog else {
797 return self
798 .execute_function_call_with_session(function_name, args, session_vars)
799 .await;
800 };
801
802 let quoted_fn = quote_postgres_identifier(function_name);
803 let sql = build_changelog_cte_sql("ed_fn, args.len(), changelog.pre_image);
806
807 let mut flex_args: Vec<FlexParam> = args
816 .iter()
817 .map(|v| match v {
818 serde_json::Value::Null => FlexParam::Null,
819 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
820 _ => FlexParam::Text(v.to_string()),
821 })
822 .collect();
823 flex_args.push(FlexParam::Text(changelog.object_type.to_string()));
824 flex_args.push(FlexParam::Text(changelog.modification_type.to_string()));
825 flex_args
828 .push(changelog.tenant_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
829 flex_args
831 .push(changelog.trace_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
832 flex_args.push(
834 changelog
835 .schema_version
836 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
837 );
838 flex_args.push(
840 changelog
841 .trace_context
842 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
843 );
844 flex_args
846 .push(changelog.actor_type.map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())));
847 flex_args
850 .push(changelog.acting_for.map_or(FlexParam::Null, |u| FlexParam::Text(u.to_string())));
851 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
852 .iter()
853 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
854 .collect();
855
856 let mut client = self.acquire_connection_with_retry().await?;
857 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
860 let txn =
861 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
862 message: format!("Failed to start change-log outbox transaction: {e}"),
863 sql_state: e.code().map(|c| c.code().to_string()),
864 })?;
865
866 apply_session_vars(&txn, session_vars).await?;
869
870 mark_cdc_mediated(&txn).await?;
873
874 if self.mutation_timing_enabled {
876 txn.execute(
877 "SELECT set_config($1, clock_timestamp()::text, true)",
878 &[&self.timing_variable_name],
879 )
880 .await
881 .map_err(|e| FraiseQLError::Database {
882 message: format!("Failed to set mutation timing variable: {e}"),
883 sql_state: e.code().map(|c| c.code().to_string()),
884 })?;
885 }
886
887 let started_at_set =
894 session_vars.iter().any(|(name, _)| *name == crate::changelog::STARTED_AT_VAR)
895 || (self.mutation_timing_enabled
896 && self.timing_variable_name == crate::changelog::STARTED_AT_VAR);
897 if !started_at_set {
898 txn.execute(
899 "SELECT set_config($1, clock_timestamp()::text, true)",
900 &[&crate::changelog::STARTED_AT_VAR],
901 )
902 .await
903 .map_err(|e| FraiseQLError::Database {
904 message: format!("Failed to stamp change-log started_at: {e}"),
905 sql_state: e.code().map(|c| c.code().to_string()),
906 })?;
907 }
908
909 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
910 let detail = e.as_db_error().map_or("", |d| d.message());
911 FraiseQLError::Database {
912 message: format!(
913 "Function call {function_name} (with change-log outbox) failed: {e}: {detail}"
914 ),
915 sql_state: e.code().map(|c| c.code().to_string()),
916 }
917 })?;
918
919 txn.commit().await.map_err(|e| FraiseQLError::Database {
920 message: format!("Failed to commit change-log outbox transaction: {e}"),
921 sql_state: e.code().map(|c| c.code().to_string()),
922 })?;
923
924 Ok(rows.iter().map(row_to_map).collect())
925 }
926
927 async fn execute_where_query_arc_with_session(
928 &self,
929 view: &str,
930 where_clause: Option<&WhereClause>,
931 limit: Option<u32>,
932 offset: Option<u32>,
933 order_by: Option<&[OrderByClause]>,
934 session_vars: &[(&str, &str)],
935 ) -> Result<Arc<Vec<JsonbValue>>> {
936 if session_vars.is_empty() {
937 return self.execute_where_query_arc(view, where_clause, limit, offset, order_by).await;
938 }
939
940 let (sql, typed_params) =
941 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
942 let param_refs = crate::types::as_sql_param_refs(&typed_params);
943
944 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
945 .await
946 .map(Arc::new)
947 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
948 }
949
950 async fn execute_with_projection_arc_with_session(
951 &self,
952 request: &ProjectionRequest<'_>,
953 session_vars: &[(&str, &str)],
954 ) -> Result<Arc<Vec<JsonbValue>>> {
955 if session_vars.is_empty() {
956 return self.execute_with_projection_arc(request).await;
957 }
958
959 let Some(projection) = request.projection else {
962 return self
963 .execute_where_query_arc_with_session(
964 request.view,
965 request.where_clause,
966 request.limit,
967 request.offset,
968 request.order_by,
969 session_vars,
970 )
971 .await;
972 };
973
974 let (sql, typed_params) = build_projection_select_sql(
975 projection,
976 request.view,
977 request.where_clause,
978 request.limit,
979 request.offset,
980 request.order_by,
981 )?;
982 let param_refs = crate::types::as_sql_param_refs(&typed_params);
983
984 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
985 .await
986 .map(Arc::new)
987 }
988
989 async fn explain_query(
990 &self,
991 sql: &str,
992 _params: &[serde_json::Value],
993 ) -> Result<serde_json::Value> {
994 if sql.contains(';') {
998 return Err(FraiseQLError::Validation {
999 message: "EXPLAIN SQL must be a single statement".into(),
1000 path: None,
1001 });
1002 }
1003 let explain_sql = format!("EXPLAIN (ANALYZE false, FORMAT JSON) {sql}");
1004 let client = self.acquire_connection_with_retry().await?;
1005 let rows: Vec<Row> =
1006 client
1007 .query(explain_sql.as_str(), &[])
1008 .await
1009 .map_err(|e| FraiseQLError::Database {
1010 message: format!("EXPLAIN failed: {e}"),
1011 sql_state: e.code().map(|c| c.code().to_string()),
1012 })?;
1013
1014 if let Some(row) = rows.first() {
1015 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
1016 message: format!("Failed to parse EXPLAIN output: {e}"),
1017 sql_state: None,
1018 })?;
1019 Ok(plan)
1020 } else {
1021 Ok(serde_json::Value::Null)
1022 }
1023 }
1024
1025 async fn query_stats(&self, limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
1026 self.pg_query_stats(limit).await
1027 }
1028
1029 async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
1030 self.pg_query_stats_by_id(id).await
1031 }
1032
1033 async fn reset_query_stats(&self) -> Result<()> {
1034 self.pg_reset_query_stats().await
1035 }
1036}
1037
1038impl SupportsMutations for PostgresAdapter {}