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_dry_run(
705 &self,
706 function_name: &str,
707 args: &[serde_json::Value],
708 session_vars: &[(&str, &str)],
709 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
710 let quoted_fn = quote_postgres_identifier(function_name);
717 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
718 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
719
720 let flex_args: Vec<FlexParam> = args
722 .iter()
723 .map(|v| match v {
724 serde_json::Value::Null => FlexParam::Null,
725 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
726 _ => FlexParam::Text(v.to_string()),
727 })
728 .collect();
729 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
730 .iter()
731 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
732 .collect();
733
734 let mut client = self.acquire_connection_with_retry().await?;
735 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
737 let txn =
738 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
739 message: format!("Failed to start dry-run transaction: {e}"),
740 sql_state: e.code().map(|c| c.code().to_string()),
741 })?;
742
743 apply_session_vars(&txn, session_vars).await?;
746 mark_cdc_mediated(&txn).await?;
749
750 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
751 let detail = e.as_db_error().map_or("", |d| d.message());
752 FraiseQLError::Database {
753 message: format!("Dry-run function call {function_name} failed: {e}: {detail}"),
754 sql_state: e.code().map(|c| c.code().to_string()),
755 }
756 })?;
757
758 txn.rollback().await.map_err(|e| FraiseQLError::Database {
760 message: format!("Failed to roll back dry-run transaction: {e}"),
761 sql_state: e.code().map(|c| c.code().to_string()),
762 })?;
763
764 Ok(rows.iter().map(row_to_map).collect())
765 }
766
767 async fn execute_function_call_with_session(
773 &self,
774 function_name: &str,
775 args: &[serde_json::Value],
776 session_vars: &[(&str, &str)],
777 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
778 if session_vars.is_empty() && !self.mutation_timing_enabled {
782 return self.execute_function_call(function_name, args).await;
783 }
784
785 let quoted_fn = quote_postgres_identifier(function_name);
786 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
787 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
788
789 let flex_args: Vec<FlexParam> = args
791 .iter()
792 .map(|v| match v {
793 serde_json::Value::Null => FlexParam::Null,
794 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
795 _ => FlexParam::Text(v.to_string()),
796 })
797 .collect();
798 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
799 .iter()
800 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
801 .collect();
802
803 let mut client = self.acquire_connection_with_retry().await?;
804 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
806 let txn =
807 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
808 message: format!("Failed to start session-var transaction: {e}"),
809 sql_state: e.code().map(|c| c.code().to_string()),
810 })?;
811
812 apply_session_vars(&txn, session_vars).await?;
814
815 mark_cdc_mediated(&txn).await?;
820
821 if self.mutation_timing_enabled {
823 txn.execute(
824 "SELECT set_config($1, clock_timestamp()::text, true)",
825 &[&self.timing_variable_name],
826 )
827 .await
828 .map_err(|e| FraiseQLError::Database {
829 message: format!("Failed to set mutation timing variable: {e}"),
830 sql_state: e.code().map(|c| c.code().to_string()),
831 })?;
832 }
833
834 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
835 let detail = e.as_db_error().map_or("", |d| d.message());
836 FraiseQLError::Database {
837 message: format!("Function call {function_name} failed: {e}: {detail}"),
838 sql_state: e.code().map(|c| c.code().to_string()),
839 }
840 })?;
841
842 txn.commit().await.map_err(|e| FraiseQLError::Database {
843 message: format!("Failed to commit session-var transaction: {e}"),
844 sql_state: e.code().map(|c| c.code().to_string()),
845 })?;
846
847 Ok(rows.iter().map(row_to_map).collect())
848 }
849
850 async fn execute_function_call_with_changelog(
851 &self,
852 function_name: &str,
853 args: &[serde_json::Value],
854 session_vars: &[(&str, &str)],
855 changelog: Option<&crate::traits::ChangeLogWrite<'_>>,
856 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
857 let Some(changelog) = changelog else {
860 return self
861 .execute_function_call_with_session(function_name, args, session_vars)
862 .await;
863 };
864
865 let quoted_fn = quote_postgres_identifier(function_name);
866 let sql = build_changelog_cte_sql("ed_fn, args.len(), changelog.pre_image);
869
870 let mut flex_args: Vec<FlexParam> = args
879 .iter()
880 .map(|v| match v {
881 serde_json::Value::Null => FlexParam::Null,
882 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
883 _ => FlexParam::Text(v.to_string()),
884 })
885 .collect();
886 flex_args.push(FlexParam::Text(changelog.object_type.to_string()));
887 flex_args.push(FlexParam::Text(changelog.modification_type.to_string()));
888 flex_args
891 .push(changelog.tenant_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
892 flex_args
894 .push(changelog.trace_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
895 flex_args.push(
897 changelog
898 .schema_version
899 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
900 );
901 flex_args.push(
903 changelog
904 .trace_context
905 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
906 );
907 flex_args
909 .push(changelog.actor_type.map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())));
910 flex_args
913 .push(changelog.acting_for.map_or(FlexParam::Null, |u| FlexParam::Text(u.to_string())));
914 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
915 .iter()
916 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
917 .collect();
918
919 let mut client = self.acquire_connection_with_retry().await?;
920 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
923 let txn =
924 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
925 message: format!("Failed to start change-log outbox transaction: {e}"),
926 sql_state: e.code().map(|c| c.code().to_string()),
927 })?;
928
929 apply_session_vars(&txn, session_vars).await?;
932
933 mark_cdc_mediated(&txn).await?;
936
937 if self.mutation_timing_enabled {
939 txn.execute(
940 "SELECT set_config($1, clock_timestamp()::text, true)",
941 &[&self.timing_variable_name],
942 )
943 .await
944 .map_err(|e| FraiseQLError::Database {
945 message: format!("Failed to set mutation timing variable: {e}"),
946 sql_state: e.code().map(|c| c.code().to_string()),
947 })?;
948 }
949
950 let started_at_set =
957 session_vars.iter().any(|(name, _)| *name == crate::changelog::STARTED_AT_VAR)
958 || (self.mutation_timing_enabled
959 && self.timing_variable_name == crate::changelog::STARTED_AT_VAR);
960 if !started_at_set {
961 txn.execute(
962 "SELECT set_config($1, clock_timestamp()::text, true)",
963 &[&crate::changelog::STARTED_AT_VAR],
964 )
965 .await
966 .map_err(|e| FraiseQLError::Database {
967 message: format!("Failed to stamp change-log started_at: {e}"),
968 sql_state: e.code().map(|c| c.code().to_string()),
969 })?;
970 }
971
972 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
973 let detail = e.as_db_error().map_or("", |d| d.message());
974 FraiseQLError::Database {
975 message: format!(
976 "Function call {function_name} (with change-log outbox) failed: {e}: {detail}"
977 ),
978 sql_state: e.code().map(|c| c.code().to_string()),
979 }
980 })?;
981
982 txn.commit().await.map_err(|e| FraiseQLError::Database {
983 message: format!("Failed to commit change-log outbox transaction: {e}"),
984 sql_state: e.code().map(|c| c.code().to_string()),
985 })?;
986
987 Ok(rows.iter().map(row_to_map).collect())
988 }
989
990 async fn execute_where_query_arc_with_session(
991 &self,
992 view: &str,
993 where_clause: Option<&WhereClause>,
994 limit: Option<u32>,
995 offset: Option<u32>,
996 order_by: Option<&[OrderByClause]>,
997 session_vars: &[(&str, &str)],
998 ) -> Result<Arc<Vec<JsonbValue>>> {
999 if session_vars.is_empty() {
1000 return self.execute_where_query_arc(view, where_clause, limit, offset, order_by).await;
1001 }
1002
1003 let (sql, typed_params) =
1004 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
1005 let param_refs = crate::types::as_sql_param_refs(&typed_params);
1006
1007 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
1008 .await
1009 .map(Arc::new)
1010 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
1011 }
1012
1013 async fn execute_with_projection_arc_with_session(
1014 &self,
1015 request: &ProjectionRequest<'_>,
1016 session_vars: &[(&str, &str)],
1017 ) -> Result<Arc<Vec<JsonbValue>>> {
1018 if session_vars.is_empty() {
1019 return self.execute_with_projection_arc(request).await;
1020 }
1021
1022 let Some(projection) = request.projection else {
1025 return self
1026 .execute_where_query_arc_with_session(
1027 request.view,
1028 request.where_clause,
1029 request.limit,
1030 request.offset,
1031 request.order_by,
1032 session_vars,
1033 )
1034 .await;
1035 };
1036
1037 let (sql, typed_params) = build_projection_select_sql(
1038 projection,
1039 request.view,
1040 request.where_clause,
1041 request.limit,
1042 request.offset,
1043 request.order_by,
1044 )?;
1045 let param_refs = crate::types::as_sql_param_refs(&typed_params);
1046
1047 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
1048 .await
1049 .map(Arc::new)
1050 }
1051
1052 async fn explain_query(
1053 &self,
1054 sql: &str,
1055 _params: &[serde_json::Value],
1056 ) -> Result<serde_json::Value> {
1057 if sql.contains(';') {
1061 return Err(FraiseQLError::Validation {
1062 message: "EXPLAIN SQL must be a single statement".into(),
1063 path: None,
1064 });
1065 }
1066 let explain_sql = format!("EXPLAIN (ANALYZE false, FORMAT JSON) {sql}");
1067 let client = self.acquire_connection_with_retry().await?;
1068 let rows: Vec<Row> =
1069 client
1070 .query(explain_sql.as_str(), &[])
1071 .await
1072 .map_err(|e| FraiseQLError::Database {
1073 message: format!("EXPLAIN failed: {e}"),
1074 sql_state: e.code().map(|c| c.code().to_string()),
1075 })?;
1076
1077 if let Some(row) = rows.first() {
1078 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
1079 message: format!("Failed to parse EXPLAIN output: {e}"),
1080 sql_state: None,
1081 })?;
1082 Ok(plan)
1083 } else {
1084 Ok(serde_json::Value::Null)
1085 }
1086 }
1087
1088 async fn query_stats(&self, limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
1089 self.pg_query_stats(limit).await
1090 }
1091
1092 async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
1093 self.pg_query_stats_by_id(id).await
1094 }
1095
1096 async fn reset_query_stats(&self) -> Result<()> {
1097 self.pg_reset_query_stats().await
1098 }
1099}
1100
1101impl SupportsMutations for PostgresAdapter {}