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::<_, i16>(idx) {
149 serde_json::json!(v)
156 } else if let Ok(v) = row.try_get::<_, i32>(idx) {
157 serde_json::json!(v)
158 } else if let Ok(v) = row.try_get::<_, i64>(idx) {
159 serde_json::json!(v)
160 } else if let Ok(v) = row.try_get::<_, f64>(idx) {
161 serde_json::json!(v)
162 } else if let Ok(v) = row.try_get::<_, String>(idx) {
163 serde_json::json!(v)
164 } else if let Ok(v) = row.try_get::<_, bool>(idx) {
165 serde_json::json!(v)
166 } else if let Ok(v) = row.try_get::<_, rust_decimal::Decimal>(idx) {
167 let s = v.to_string();
174 serde_json::from_str::<serde_json::Value>(&s).unwrap_or(serde_json::Value::String(s))
175 } else if let Ok(v) = row.try_get::<_, uuid::Uuid>(idx) {
176 serde_json::json!(v.to_string())
179 } else if let Ok(v) = row.try_get::<_, chrono::DateTime<chrono::Utc>>(idx) {
180 serde_json::json!(v.to_rfc3339())
182 } else if let Ok(v) = row.try_get::<_, chrono::NaiveDateTime>(idx) {
183 serde_json::json!(v.to_string())
185 } else if let Ok(v) = row.try_get::<_, chrono::NaiveDate>(idx) {
186 serde_json::json!(v.to_string())
188 } else if let Ok(v) = row.try_get::<_, Vec<String>>(idx) {
189 serde_json::json!(v)
194 } else if let Ok(v) = row.try_get::<_, serde_json::Value>(idx) {
195 v
196 } else {
197 tracing::warn!(
201 column = %column_name,
202 pg_type = %column.type_(),
203 "row_to_map: column type not representable as JSON; returning null"
204 );
205 serde_json::Value::Null
206 };
207 map.insert(column_name, value);
208 }
209 map
210}
211
212pub(super) async fn apply_session_vars(
218 txn: &tokio_postgres::Transaction<'_>,
219 session_vars: &[(&str, &str)],
220) -> Result<()> {
221 for (name, value) in session_vars {
222 if *value == crate::changelog::CLOCK_TIMESTAMP_DIRECTIVE {
227 txn.execute("SELECT set_config($1, clock_timestamp()::text, true)", &[name])
228 .await
229 .map_err(|e| FraiseQLError::Database {
230 message: format!("set_config({name:?}, clock_timestamp()) failed: {e}"),
231 sql_state: e.code().map(|c| c.code().to_string()),
232 })?;
233 } else {
234 txn.execute("SELECT set_config($1, $2, true)", &[name, value])
235 .await
236 .map_err(|e| FraiseQLError::Database {
237 message: format!("set_config({name:?}) failed: {e}"),
238 sql_state: e.code().map(|c| c.code().to_string()),
239 })?;
240 }
241 }
242 Ok(())
243}
244
245pub(super) async fn mark_cdc_mediated(txn: &tokio_postgres::Transaction<'_>) -> Result<()> {
257 txn.execute(
258 "SELECT set_config($1, $2, true)",
259 &[
260 &crate::changelog::CDC_MEDIATED_VAR,
261 &crate::changelog::CDC_MEDIATED_ON,
262 ],
263 )
264 .await
265 .map_err(|e| FraiseQLError::Database {
266 message: format!("Failed to set {} marker: {e}", crate::changelog::CDC_MEDIATED_VAR),
267 sql_state: e.code().map(|c| c.code().to_string()),
268 })?;
269 Ok(())
270}
271
272pub(super) fn build_changelog_cte_sql(quoted_fn: &str, n_args: usize, pre_image: bool) -> String {
301 let placeholders: Vec<String> = (1..=n_args).map(|i| format!("${i}")).collect();
302 let object_type_idx = n_args + 1;
303 let modification_type_idx = n_args + 2;
304 let tenant_id_idx = n_args + 3;
305 let trace_id_idx = n_args + 4;
306 let schema_version_idx = n_args + 5;
307 let trace_context_idx = n_args + 6;
308 let actor_type_idx = n_args + 7;
309 let acting_for_idx = n_args + 8;
310 let started_var = crate::changelog::STARTED_AT_VAR;
311 let duration = crate::changelog::duration_ms_sql(started_var);
312 let calc_version = crate::changelog::DURATION_CALC_VERSION;
313 let after_before_cols = if pre_image {
322 "object_data, object_data_before"
323 } else {
324 "object_data"
325 };
326 let after_before_vals = if pre_image {
327 "r.entity, r.entity_before"
328 } else {
329 "r.entity"
330 };
331 format!(
332 "WITH r AS MATERIALIZED (SELECT * FROM {quoted_fn}({args})), \
333 _changelog AS ( \
334 INSERT INTO core.tb_entity_change_log \
335 (object_type, modification_type, object_id, {after_before_cols}, \
336 updated_fields, cascade, started_at, duration_ms, extra_metadata, \
337 tenant_id, trace_id, schema_version, trace_context, \
338 actor_type, acting_for, commit_time) \
339 SELECT \
340 COALESCE(r.entity_type, ${object_type_idx}), \
341 ${modification_type_idx}, \
342 r.entity_id, {after_before_vals}, r.updated_fields, r.cascade, \
343 current_setting('{started_var}')::timestamptz, \
344 {duration}, \
345 jsonb_build_object('duration_calc_version', {calc_version}::int), \
346 ${tenant_id_idx}::uuid, \
347 ${trace_id_idx}, \
348 ${schema_version_idx}, \
349 ${trace_context_idx}::jsonb, \
350 ${actor_type_idx}, \
351 ${acting_for_idx}::uuid, \
352 clock_timestamp() \
353 FROM r \
354 WHERE r.succeeded AND r.state_changed \
355 RETURNING 1 \
356 ) \
357 SELECT * FROM r",
358 args = placeholders.join(", "),
359 )
360}
361
362async fn prepare_cached_stmt(
374 client: &deadpool_postgres::Client,
375 sql: &str,
376) -> Result<tokio_postgres::Statement> {
377 client.prepare_cached(sql).await.map_err(|e| FraiseQLError::Database {
378 message: format!("Failed to prepare statement: {e}"),
379 sql_state: e.code().map(|c| c.code().to_string()),
380 })
381}
382
383#[async_trait]
387impl DatabaseAdapter for PostgresAdapter {
388 async fn execute_with_projection(
389 &self,
390 view: &str,
391 projection: Option<&SqlProjectionHint>,
392 where_clause: Option<&WhereClause>,
393 limit: Option<u32>,
394 offset: Option<u32>,
395 order_by: Option<&[OrderByClause]>,
396 ) -> Result<Vec<JsonbValue>> {
397 self.execute_with_projection_impl(view, projection, where_clause, limit, offset, order_by)
398 .await
399 }
400
401 async fn execute_where_query(
402 &self,
403 view: &str,
404 where_clause: Option<&WhereClause>,
405 limit: Option<u32>,
406 offset: Option<u32>,
407 order_by: Option<&[OrderByClause]>,
408 ) -> Result<Vec<JsonbValue>> {
409 let (sql, typed_params) =
410 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
411
412 let param_refs = crate::types::as_sql_param_refs(&typed_params);
413
414 self.execute_raw(&sql, ¶m_refs)
415 .await
416 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
417 }
418
419 async fn explain_where_query(
420 &self,
421 view: &str,
422 where_clause: Option<&WhereClause>,
423 limit: Option<u32>,
424 offset: Option<u32>,
425 ) -> Result<serde_json::Value> {
426 let (select_sql, typed_params) = build_where_select_sql(view, where_clause, limit, offset)?;
427 if select_sql.contains(';') {
430 return Err(FraiseQLError::Validation {
431 message: "EXPLAIN SQL must be a single statement".into(),
432 path: None,
433 });
434 }
435 let explain_sql = format!("EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) {select_sql}");
436
437 let param_refs = crate::types::as_sql_param_refs(&typed_params);
438
439 let client = self.acquire_connection_with_retry().await?;
440 let rows = client.query(explain_sql.as_str(), ¶m_refs).await.map_err(|e| {
441 FraiseQLError::Database {
442 message: format!("EXPLAIN ANALYZE failed: {e}"),
443 sql_state: e.code().map(|c| c.code().to_string()),
444 }
445 })?;
446
447 if let Some(row) = rows.first() {
448 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
449 message: format!("Failed to parse EXPLAIN output: {e}"),
450 sql_state: None,
451 })?;
452 Ok(plan)
453 } else {
454 Ok(serde_json::Value::Null)
455 }
456 }
457
458 fn database_type(&self) -> DatabaseType {
459 DatabaseType::PostgreSQL
460 }
461
462 async fn health_check(&self) -> Result<()> {
463 let client = self.acquire_connection_with_retry().await?;
465
466 client.query("SELECT 1", &[]).await.map_err(|e| FraiseQLError::Database {
467 message: format!("Health check failed: {e}"),
468 sql_state: e.code().map(|c| c.code().to_string()),
469 })?;
470
471 Ok(())
472 }
473
474 #[allow(clippy::cast_possible_truncation)] fn pool_metrics(&self) -> PoolMetrics {
476 let status = self.pool.status();
477
478 PoolMetrics {
479 total_connections: status.size as u32,
480 idle_connections: status.available as u32,
481 active_connections: (status.size - status.available) as u32,
482 waiting_requests: status.waiting as u32,
483 }
484 }
485
486 async fn execute_raw_query(
491 &self,
492 sql: &str,
493 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
494 let client = self.acquire_connection_with_retry().await?;
496
497 let rows: Vec<Row> = client.query(sql, &[]).await.map_err(|e| FraiseQLError::Database {
498 message: format!("Query execution failed: {e}"),
499 sql_state: e.code().map(|c| c.code().to_string()),
500 })?;
501
502 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
504 rows.iter().map(row_to_map).collect();
505
506 Ok(results)
507 }
508
509 async fn execute_parameterized_aggregate(
510 &self,
511 sql: &str,
512 params: &[serde_json::Value],
513 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
514 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
518 let param_refs = crate::types::as_sql_param_refs(&typed);
519
520 let client = self.acquire_connection_with_retry().await?;
521 let rows: Vec<Row> =
522 client.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
523 message: format!("Parameterized aggregate query failed: {e}"),
524 sql_state: e.code().map(|c| c.code().to_string()),
525 })?;
526
527 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
528 rows.iter().map(row_to_map).collect();
529
530 Ok(results)
531 }
532
533 async fn execute_parameterized_aggregate_with_session(
534 &self,
535 sql: &str,
536 params: &[serde_json::Value],
537 session_vars: &[(&str, &str)],
538 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
539 if session_vars.is_empty() {
540 return self.execute_parameterized_aggregate(sql, params).await;
541 }
542
543 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
544 let param_refs = crate::types::as_sql_param_refs(&typed);
545
546 let mut client = self.acquire_connection_with_retry().await?;
547 let txn =
548 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
549 message: format!("Failed to start session-var transaction: {e}"),
550 sql_state: e.code().map(|c| c.code().to_string()),
551 })?;
552 apply_session_vars(&txn, session_vars).await?;
553 let rows: Vec<Row> =
554 txn.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
555 message: format!("Parameterized aggregate query failed: {e}"),
556 sql_state: e.code().map(|c| c.code().to_string()),
557 })?;
558 txn.commit().await.map_err(|e| FraiseQLError::Database {
559 message: format!("Failed to commit session-var transaction: {e}"),
560 sql_state: e.code().map(|c| c.code().to_string()),
561 })?;
562
563 Ok(rows.iter().map(row_to_map).collect())
564 }
565
566 async fn execute_function_call(
567 &self,
568 function_name: &str,
569 args: &[serde_json::Value],
570 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
571 let quoted_fn = quote_postgres_identifier(function_name);
577 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
578 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
579
580 let mut client = self.acquire_connection_with_retry().await?;
581
582 let flex_args: Vec<FlexParam> = args
590 .iter()
591 .map(|v| match v {
592 serde_json::Value::Null => FlexParam::Null,
593 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
594 _ => FlexParam::Text(v.to_string()),
595 })
596 .collect();
597 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
598 .iter()
599 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
600 .collect();
601
602 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
606
607 if self.mutation_timing_enabled {
608 let txn =
612 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
613 message: format!("Failed to start mutation timing transaction: {e}"),
614 sql_state: e.code().map(|c| c.code().to_string()),
615 })?;
616
617 txn.execute(
618 "SELECT set_config($1, clock_timestamp()::text, true)",
619 &[&self.timing_variable_name],
620 )
621 .await
622 .map_err(|e| FraiseQLError::Database {
623 message: format!("Failed to set mutation timing variable: {e}"),
624 sql_state: e.code().map(|c| c.code().to_string()),
625 })?;
626
627 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
628 let detail = e.as_db_error().map_or("", |d| d.message());
629 FraiseQLError::Database {
630 message: format!("Function call {function_name} failed: {e}: {detail}"),
631 sql_state: e.code().map(|c| c.code().to_string()),
632 }
633 })?;
634
635 txn.commit().await.map_err(|e| FraiseQLError::Database {
636 message: format!("Failed to commit mutation timing transaction: {e}"),
637 sql_state: e.code().map(|c| c.code().to_string()),
638 })?;
639
640 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
641 rows.iter().map(row_to_map).collect();
642
643 Ok(results)
644 } else {
645 let rows: Vec<Row> = client.query(&stmt, params.as_slice()).await.map_err(|e| {
646 let detail = e.as_db_error().map_or("", |d| d.message());
647 FraiseQLError::Database {
648 message: format!("Function call {function_name} failed: {e}: {detail}"),
649 sql_state: e.code().map(|c| c.code().to_string()),
650 }
651 })?;
652
653 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
654 rows.iter().map(row_to_map).collect();
655
656 Ok(results)
657 }
658 }
659
660 async fn execute_function_call_with_session(
666 &self,
667 function_name: &str,
668 args: &[serde_json::Value],
669 session_vars: &[(&str, &str)],
670 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
671 if session_vars.is_empty() && !self.mutation_timing_enabled {
675 return self.execute_function_call(function_name, args).await;
676 }
677
678 let quoted_fn = quote_postgres_identifier(function_name);
679 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
680 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
681
682 let flex_args: Vec<FlexParam> = args
684 .iter()
685 .map(|v| match v {
686 serde_json::Value::Null => FlexParam::Null,
687 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
688 _ => FlexParam::Text(v.to_string()),
689 })
690 .collect();
691 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
692 .iter()
693 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
694 .collect();
695
696 let mut client = self.acquire_connection_with_retry().await?;
697 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
699 let txn =
700 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
701 message: format!("Failed to start session-var transaction: {e}"),
702 sql_state: e.code().map(|c| c.code().to_string()),
703 })?;
704
705 apply_session_vars(&txn, session_vars).await?;
707
708 mark_cdc_mediated(&txn).await?;
713
714 if self.mutation_timing_enabled {
716 txn.execute(
717 "SELECT set_config($1, clock_timestamp()::text, true)",
718 &[&self.timing_variable_name],
719 )
720 .await
721 .map_err(|e| FraiseQLError::Database {
722 message: format!("Failed to set mutation timing variable: {e}"),
723 sql_state: e.code().map(|c| c.code().to_string()),
724 })?;
725 }
726
727 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
728 let detail = e.as_db_error().map_or("", |d| d.message());
729 FraiseQLError::Database {
730 message: format!("Function call {function_name} failed: {e}: {detail}"),
731 sql_state: e.code().map(|c| c.code().to_string()),
732 }
733 })?;
734
735 txn.commit().await.map_err(|e| FraiseQLError::Database {
736 message: format!("Failed to commit session-var transaction: {e}"),
737 sql_state: e.code().map(|c| c.code().to_string()),
738 })?;
739
740 Ok(rows.iter().map(row_to_map).collect())
741 }
742
743 async fn execute_function_call_with_changelog(
744 &self,
745 function_name: &str,
746 args: &[serde_json::Value],
747 session_vars: &[(&str, &str)],
748 changelog: Option<&crate::traits::ChangeLogWrite<'_>>,
749 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
750 let Some(changelog) = changelog else {
753 return self
754 .execute_function_call_with_session(function_name, args, session_vars)
755 .await;
756 };
757
758 let quoted_fn = quote_postgres_identifier(function_name);
759 let sql = build_changelog_cte_sql("ed_fn, args.len(), changelog.pre_image);
762
763 let mut flex_args: Vec<FlexParam> = args
772 .iter()
773 .map(|v| match v {
774 serde_json::Value::Null => FlexParam::Null,
775 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
776 _ => FlexParam::Text(v.to_string()),
777 })
778 .collect();
779 flex_args.push(FlexParam::Text(changelog.object_type.to_string()));
780 flex_args.push(FlexParam::Text(changelog.modification_type.to_string()));
781 flex_args
784 .push(changelog.tenant_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
785 flex_args
787 .push(changelog.trace_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
788 flex_args.push(
790 changelog
791 .schema_version
792 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
793 );
794 flex_args.push(
796 changelog
797 .trace_context
798 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
799 );
800 flex_args
802 .push(changelog.actor_type.map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())));
803 flex_args
806 .push(changelog.acting_for.map_or(FlexParam::Null, |u| FlexParam::Text(u.to_string())));
807 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
808 .iter()
809 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
810 .collect();
811
812 let mut client = self.acquire_connection_with_retry().await?;
813 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
816 let txn =
817 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
818 message: format!("Failed to start change-log outbox transaction: {e}"),
819 sql_state: e.code().map(|c| c.code().to_string()),
820 })?;
821
822 apply_session_vars(&txn, session_vars).await?;
825
826 mark_cdc_mediated(&txn).await?;
829
830 if self.mutation_timing_enabled {
832 txn.execute(
833 "SELECT set_config($1, clock_timestamp()::text, true)",
834 &[&self.timing_variable_name],
835 )
836 .await
837 .map_err(|e| FraiseQLError::Database {
838 message: format!("Failed to set mutation timing variable: {e}"),
839 sql_state: e.code().map(|c| c.code().to_string()),
840 })?;
841 }
842
843 let started_at_set =
850 session_vars.iter().any(|(name, _)| *name == crate::changelog::STARTED_AT_VAR)
851 || (self.mutation_timing_enabled
852 && self.timing_variable_name == crate::changelog::STARTED_AT_VAR);
853 if !started_at_set {
854 txn.execute(
855 "SELECT set_config($1, clock_timestamp()::text, true)",
856 &[&crate::changelog::STARTED_AT_VAR],
857 )
858 .await
859 .map_err(|e| FraiseQLError::Database {
860 message: format!("Failed to stamp change-log started_at: {e}"),
861 sql_state: e.code().map(|c| c.code().to_string()),
862 })?;
863 }
864
865 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
866 let detail = e.as_db_error().map_or("", |d| d.message());
867 FraiseQLError::Database {
868 message: format!(
869 "Function call {function_name} (with change-log outbox) failed: {e}: {detail}"
870 ),
871 sql_state: e.code().map(|c| c.code().to_string()),
872 }
873 })?;
874
875 txn.commit().await.map_err(|e| FraiseQLError::Database {
876 message: format!("Failed to commit change-log outbox transaction: {e}"),
877 sql_state: e.code().map(|c| c.code().to_string()),
878 })?;
879
880 Ok(rows.iter().map(row_to_map).collect())
881 }
882
883 async fn execute_where_query_arc_with_session(
884 &self,
885 view: &str,
886 where_clause: Option<&WhereClause>,
887 limit: Option<u32>,
888 offset: Option<u32>,
889 order_by: Option<&[OrderByClause]>,
890 session_vars: &[(&str, &str)],
891 ) -> Result<Arc<Vec<JsonbValue>>> {
892 if session_vars.is_empty() {
893 return self.execute_where_query_arc(view, where_clause, limit, offset, order_by).await;
894 }
895
896 let (sql, typed_params) =
897 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
898 let param_refs = crate::types::as_sql_param_refs(&typed_params);
899
900 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
901 .await
902 .map(Arc::new)
903 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
904 }
905
906 async fn execute_with_projection_arc_with_session(
907 &self,
908 request: &ProjectionRequest<'_>,
909 session_vars: &[(&str, &str)],
910 ) -> Result<Arc<Vec<JsonbValue>>> {
911 if session_vars.is_empty() {
912 return self.execute_with_projection_arc(request).await;
913 }
914
915 let Some(projection) = request.projection else {
918 return self
919 .execute_where_query_arc_with_session(
920 request.view,
921 request.where_clause,
922 request.limit,
923 request.offset,
924 request.order_by,
925 session_vars,
926 )
927 .await;
928 };
929
930 let (sql, typed_params) = build_projection_select_sql(
931 projection,
932 request.view,
933 request.where_clause,
934 request.limit,
935 request.offset,
936 request.order_by,
937 )?;
938 let param_refs = crate::types::as_sql_param_refs(&typed_params);
939
940 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
941 .await
942 .map(Arc::new)
943 }
944
945 async fn explain_query(
946 &self,
947 sql: &str,
948 _params: &[serde_json::Value],
949 ) -> Result<serde_json::Value> {
950 if sql.contains(';') {
954 return Err(FraiseQLError::Validation {
955 message: "EXPLAIN SQL must be a single statement".into(),
956 path: None,
957 });
958 }
959 let explain_sql = format!("EXPLAIN (ANALYZE false, FORMAT JSON) {sql}");
960 let client = self.acquire_connection_with_retry().await?;
961 let rows: Vec<Row> =
962 client
963 .query(explain_sql.as_str(), &[])
964 .await
965 .map_err(|e| FraiseQLError::Database {
966 message: format!("EXPLAIN failed: {e}"),
967 sql_state: e.code().map(|c| c.code().to_string()),
968 })?;
969
970 if let Some(row) = rows.first() {
971 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
972 message: format!("Failed to parse EXPLAIN output: {e}"),
973 sql_state: None,
974 })?;
975 Ok(plan)
976 } else {
977 Ok(serde_json::Value::Null)
978 }
979 }
980
981 async fn query_stats(&self, limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
982 self.pg_query_stats(limit).await
983 }
984
985 async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
986 self.pg_query_stats_by_id(id).await
987 }
988
989 async fn reset_query_stats(&self) -> Result<()> {
990 self.pg_reset_query_stats().await
991 }
992}
993
994impl SupportsMutations for PostgresAdapter {}