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) async fn mark_cdc_mediated(txn: &tokio_postgres::Transaction<'_>) -> Result<()> {
219 txn.execute(
220 "SELECT set_config($1, $2, true)",
221 &[
222 &crate::changelog::CDC_MEDIATED_VAR,
223 &crate::changelog::CDC_MEDIATED_ON,
224 ],
225 )
226 .await
227 .map_err(|e| FraiseQLError::Database {
228 message: format!("Failed to set {} marker: {e}", crate::changelog::CDC_MEDIATED_VAR),
229 sql_state: e.code().map(|c| c.code().to_string()),
230 })?;
231 Ok(())
232}
233
234pub(super) fn build_changelog_cte_sql(quoted_fn: &str, n_args: usize) -> String {
263 let placeholders: Vec<String> = (1..=n_args).map(|i| format!("${i}")).collect();
264 let object_type_idx = n_args + 1;
265 let modification_type_idx = n_args + 2;
266 let tenant_id_idx = n_args + 3;
267 let trace_id_idx = n_args + 4;
268 let schema_version_idx = n_args + 5;
269 let trace_context_idx = n_args + 6;
270 let actor_type_idx = n_args + 7;
271 let acting_for_idx = n_args + 8;
272 let started_var = crate::changelog::STARTED_AT_VAR;
273 let duration = crate::changelog::duration_ms_sql(started_var);
274 let calc_version = crate::changelog::DURATION_CALC_VERSION;
275 format!(
276 "WITH r AS MATERIALIZED (SELECT * FROM {quoted_fn}({args})), \
277 _changelog AS ( \
278 INSERT INTO core.tb_entity_change_log \
279 (object_type, modification_type, object_id, object_data, \
280 updated_fields, cascade, started_at, duration_ms, extra_metadata, \
281 tenant_id, trace_id, schema_version, trace_context, \
282 actor_type, acting_for, commit_time) \
283 SELECT \
284 COALESCE(r.entity_type, ${object_type_idx}), \
285 ${modification_type_idx}, \
286 r.entity_id, r.entity, r.updated_fields, r.cascade, \
287 current_setting('{started_var}')::timestamptz, \
288 {duration}, \
289 jsonb_build_object('duration_calc_version', {calc_version}::int), \
290 ${tenant_id_idx}::uuid, \
291 ${trace_id_idx}, \
292 ${schema_version_idx}, \
293 ${trace_context_idx}::jsonb, \
294 ${actor_type_idx}, \
295 ${acting_for_idx}::uuid, \
296 clock_timestamp() \
297 FROM r \
298 WHERE r.succeeded AND r.state_changed \
299 RETURNING 1 \
300 ) \
301 SELECT * FROM r",
302 args = placeholders.join(", "),
303 )
304}
305
306async fn prepare_cached_stmt(
318 client: &deadpool_postgres::Client,
319 sql: &str,
320) -> Result<tokio_postgres::Statement> {
321 client.prepare_cached(sql).await.map_err(|e| FraiseQLError::Database {
322 message: format!("Failed to prepare statement: {e}"),
323 sql_state: e.code().map(|c| c.code().to_string()),
324 })
325}
326
327#[async_trait]
331impl DatabaseAdapter for PostgresAdapter {
332 async fn execute_with_projection(
333 &self,
334 view: &str,
335 projection: Option<&SqlProjectionHint>,
336 where_clause: Option<&WhereClause>,
337 limit: Option<u32>,
338 offset: Option<u32>,
339 order_by: Option<&[OrderByClause]>,
340 ) -> Result<Vec<JsonbValue>> {
341 self.execute_with_projection_impl(view, projection, where_clause, limit, offset, order_by)
342 .await
343 }
344
345 async fn execute_where_query(
346 &self,
347 view: &str,
348 where_clause: Option<&WhereClause>,
349 limit: Option<u32>,
350 offset: Option<u32>,
351 order_by: Option<&[OrderByClause]>,
352 ) -> Result<Vec<JsonbValue>> {
353 let (sql, typed_params) =
354 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
355
356 let param_refs = crate::types::as_sql_param_refs(&typed_params);
357
358 self.execute_raw(&sql, ¶m_refs)
359 .await
360 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
361 }
362
363 async fn explain_where_query(
364 &self,
365 view: &str,
366 where_clause: Option<&WhereClause>,
367 limit: Option<u32>,
368 offset: Option<u32>,
369 ) -> Result<serde_json::Value> {
370 let (select_sql, typed_params) = build_where_select_sql(view, where_clause, limit, offset)?;
371 if select_sql.contains(';') {
374 return Err(FraiseQLError::Validation {
375 message: "EXPLAIN SQL must be a single statement".into(),
376 path: None,
377 });
378 }
379 let explain_sql = format!("EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) {select_sql}");
380
381 let param_refs = crate::types::as_sql_param_refs(&typed_params);
382
383 let client = self.acquire_connection_with_retry().await?;
384 let rows = client.query(explain_sql.as_str(), ¶m_refs).await.map_err(|e| {
385 FraiseQLError::Database {
386 message: format!("EXPLAIN ANALYZE failed: {e}"),
387 sql_state: e.code().map(|c| c.code().to_string()),
388 }
389 })?;
390
391 if let Some(row) = rows.first() {
392 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
393 message: format!("Failed to parse EXPLAIN output: {e}"),
394 sql_state: None,
395 })?;
396 Ok(plan)
397 } else {
398 Ok(serde_json::Value::Null)
399 }
400 }
401
402 fn database_type(&self) -> DatabaseType {
403 DatabaseType::PostgreSQL
404 }
405
406 async fn health_check(&self) -> Result<()> {
407 let client = self.acquire_connection_with_retry().await?;
409
410 client.query("SELECT 1", &[]).await.map_err(|e| FraiseQLError::Database {
411 message: format!("Health check failed: {e}"),
412 sql_state: e.code().map(|c| c.code().to_string()),
413 })?;
414
415 Ok(())
416 }
417
418 #[allow(clippy::cast_possible_truncation)] fn pool_metrics(&self) -> PoolMetrics {
420 let status = self.pool.status();
421
422 PoolMetrics {
423 total_connections: status.size as u32,
424 idle_connections: status.available as u32,
425 active_connections: (status.size - status.available) as u32,
426 waiting_requests: status.waiting as u32,
427 }
428 }
429
430 async fn execute_raw_query(
435 &self,
436 sql: &str,
437 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
438 let client = self.acquire_connection_with_retry().await?;
440
441 let rows: Vec<Row> = client.query(sql, &[]).await.map_err(|e| FraiseQLError::Database {
442 message: format!("Query execution failed: {e}"),
443 sql_state: e.code().map(|c| c.code().to_string()),
444 })?;
445
446 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
448 rows.iter().map(row_to_map).collect();
449
450 Ok(results)
451 }
452
453 async fn execute_parameterized_aggregate(
454 &self,
455 sql: &str,
456 params: &[serde_json::Value],
457 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
458 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
462 let param_refs = crate::types::as_sql_param_refs(&typed);
463
464 let client = self.acquire_connection_with_retry().await?;
465 let rows: Vec<Row> =
466 client.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
471 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
472 rows.iter().map(row_to_map).collect();
473
474 Ok(results)
475 }
476
477 async fn execute_parameterized_aggregate_with_session(
478 &self,
479 sql: &str,
480 params: &[serde_json::Value],
481 session_vars: &[(&str, &str)],
482 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
483 if session_vars.is_empty() {
484 return self.execute_parameterized_aggregate(sql, params).await;
485 }
486
487 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
488 let param_refs = crate::types::as_sql_param_refs(&typed);
489
490 let mut client = self.acquire_connection_with_retry().await?;
491 let txn =
492 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
493 message: format!("Failed to start session-var transaction: {e}"),
494 sql_state: e.code().map(|c| c.code().to_string()),
495 })?;
496 apply_session_vars(&txn, session_vars).await?;
497 let rows: Vec<Row> =
498 txn.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
499 message: format!("Parameterized aggregate query failed: {e}"),
500 sql_state: e.code().map(|c| c.code().to_string()),
501 })?;
502 txn.commit().await.map_err(|e| FraiseQLError::Database {
503 message: format!("Failed to commit session-var transaction: {e}"),
504 sql_state: e.code().map(|c| c.code().to_string()),
505 })?;
506
507 Ok(rows.iter().map(row_to_map).collect())
508 }
509
510 async fn execute_function_call(
511 &self,
512 function_name: &str,
513 args: &[serde_json::Value],
514 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
515 let quoted_fn = quote_postgres_identifier(function_name);
521 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
522 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
523
524 let mut client = self.acquire_connection_with_retry().await?;
525
526 let flex_args: Vec<FlexParam> = args
534 .iter()
535 .map(|v| match v {
536 serde_json::Value::Null => FlexParam::Null,
537 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
538 _ => FlexParam::Text(v.to_string()),
539 })
540 .collect();
541 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
542 .iter()
543 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
544 .collect();
545
546 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
550
551 if self.mutation_timing_enabled {
552 let txn =
556 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
557 message: format!("Failed to start mutation timing transaction: {e}"),
558 sql_state: e.code().map(|c| c.code().to_string()),
559 })?;
560
561 txn.execute(
562 "SELECT set_config($1, clock_timestamp()::text, true)",
563 &[&self.timing_variable_name],
564 )
565 .await
566 .map_err(|e| FraiseQLError::Database {
567 message: format!("Failed to set mutation timing variable: {e}"),
568 sql_state: e.code().map(|c| c.code().to_string()),
569 })?;
570
571 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
572 let detail = e.as_db_error().map_or("", |d| d.message());
573 FraiseQLError::Database {
574 message: format!("Function call {function_name} failed: {e}: {detail}"),
575 sql_state: e.code().map(|c| c.code().to_string()),
576 }
577 })?;
578
579 txn.commit().await.map_err(|e| FraiseQLError::Database {
580 message: format!("Failed to commit mutation timing transaction: {e}"),
581 sql_state: e.code().map(|c| c.code().to_string()),
582 })?;
583
584 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
585 rows.iter().map(row_to_map).collect();
586
587 Ok(results)
588 } else {
589 let rows: Vec<Row> = client.query(&stmt, params.as_slice()).await.map_err(|e| {
590 let detail = e.as_db_error().map_or("", |d| d.message());
591 FraiseQLError::Database {
592 message: format!("Function call {function_name} failed: {e}: {detail}"),
593 sql_state: e.code().map(|c| c.code().to_string()),
594 }
595 })?;
596
597 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
598 rows.iter().map(row_to_map).collect();
599
600 Ok(results)
601 }
602 }
603
604 async fn execute_function_call_with_session(
610 &self,
611 function_name: &str,
612 args: &[serde_json::Value],
613 session_vars: &[(&str, &str)],
614 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
615 if session_vars.is_empty() && !self.mutation_timing_enabled {
619 return self.execute_function_call(function_name, args).await;
620 }
621
622 let quoted_fn = quote_postgres_identifier(function_name);
623 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
624 let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
625
626 let flex_args: Vec<FlexParam> = args
628 .iter()
629 .map(|v| match v {
630 serde_json::Value::Null => FlexParam::Null,
631 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
632 _ => FlexParam::Text(v.to_string()),
633 })
634 .collect();
635 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
636 .iter()
637 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
638 .collect();
639
640 let mut client = self.acquire_connection_with_retry().await?;
641 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
643 let txn =
644 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
645 message: format!("Failed to start session-var transaction: {e}"),
646 sql_state: e.code().map(|c| c.code().to_string()),
647 })?;
648
649 apply_session_vars(&txn, session_vars).await?;
651
652 mark_cdc_mediated(&txn).await?;
657
658 if self.mutation_timing_enabled {
660 txn.execute(
661 "SELECT set_config($1, clock_timestamp()::text, true)",
662 &[&self.timing_variable_name],
663 )
664 .await
665 .map_err(|e| FraiseQLError::Database {
666 message: format!("Failed to set mutation timing variable: {e}"),
667 sql_state: e.code().map(|c| c.code().to_string()),
668 })?;
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 session-var transaction: {e}"),
681 sql_state: e.code().map(|c| c.code().to_string()),
682 })?;
683
684 Ok(rows.iter().map(row_to_map).collect())
685 }
686
687 async fn execute_function_call_with_changelog(
688 &self,
689 function_name: &str,
690 args: &[serde_json::Value],
691 session_vars: &[(&str, &str)],
692 changelog: Option<&crate::traits::ChangeLogWrite<'_>>,
693 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
694 let Some(changelog) = changelog else {
697 return self
698 .execute_function_call_with_session(function_name, args, session_vars)
699 .await;
700 };
701
702 let quoted_fn = quote_postgres_identifier(function_name);
703 let sql = build_changelog_cte_sql("ed_fn, args.len());
706
707 let mut flex_args: Vec<FlexParam> = args
716 .iter()
717 .map(|v| match v {
718 serde_json::Value::Null => FlexParam::Null,
719 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
720 _ => FlexParam::Text(v.to_string()),
721 })
722 .collect();
723 flex_args.push(FlexParam::Text(changelog.object_type.to_string()));
724 flex_args.push(FlexParam::Text(changelog.modification_type.to_string()));
725 flex_args
728 .push(changelog.tenant_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
729 flex_args
731 .push(changelog.trace_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
732 flex_args.push(
734 changelog
735 .schema_version
736 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
737 );
738 flex_args.push(
740 changelog
741 .trace_context
742 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
743 );
744 flex_args
746 .push(changelog.actor_type.map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())));
747 flex_args
750 .push(changelog.acting_for.map_or(FlexParam::Null, |u| FlexParam::Text(u.to_string())));
751 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
752 .iter()
753 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
754 .collect();
755
756 let mut client = self.acquire_connection_with_retry().await?;
757 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
760 let txn =
761 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
762 message: format!("Failed to start change-log outbox transaction: {e}"),
763 sql_state: e.code().map(|c| c.code().to_string()),
764 })?;
765
766 apply_session_vars(&txn, session_vars).await?;
769
770 mark_cdc_mediated(&txn).await?;
773
774 if self.mutation_timing_enabled {
776 txn.execute(
777 "SELECT set_config($1, clock_timestamp()::text, true)",
778 &[&self.timing_variable_name],
779 )
780 .await
781 .map_err(|e| FraiseQLError::Database {
782 message: format!("Failed to set mutation timing variable: {e}"),
783 sql_state: e.code().map(|c| c.code().to_string()),
784 })?;
785 }
786
787 let started_at_set =
794 session_vars.iter().any(|(name, _)| *name == crate::changelog::STARTED_AT_VAR)
795 || (self.mutation_timing_enabled
796 && self.timing_variable_name == crate::changelog::STARTED_AT_VAR);
797 if !started_at_set {
798 txn.execute(
799 "SELECT set_config($1, clock_timestamp()::text, true)",
800 &[&crate::changelog::STARTED_AT_VAR],
801 )
802 .await
803 .map_err(|e| FraiseQLError::Database {
804 message: format!("Failed to stamp change-log started_at: {e}"),
805 sql_state: e.code().map(|c| c.code().to_string()),
806 })?;
807 }
808
809 let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
810 let detail = e.as_db_error().map_or("", |d| d.message());
811 FraiseQLError::Database {
812 message: format!(
813 "Function call {function_name} (with change-log outbox) failed: {e}: {detail}"
814 ),
815 sql_state: e.code().map(|c| c.code().to_string()),
816 }
817 })?;
818
819 txn.commit().await.map_err(|e| FraiseQLError::Database {
820 message: format!("Failed to commit change-log outbox transaction: {e}"),
821 sql_state: e.code().map(|c| c.code().to_string()),
822 })?;
823
824 Ok(rows.iter().map(row_to_map).collect())
825 }
826
827 async fn execute_where_query_arc_with_session(
828 &self,
829 view: &str,
830 where_clause: Option<&WhereClause>,
831 limit: Option<u32>,
832 offset: Option<u32>,
833 order_by: Option<&[OrderByClause]>,
834 session_vars: &[(&str, &str)],
835 ) -> Result<Arc<Vec<JsonbValue>>> {
836 if session_vars.is_empty() {
837 return self.execute_where_query_arc(view, where_clause, limit, offset, order_by).await;
838 }
839
840 let (sql, typed_params) =
841 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
842 let param_refs = crate::types::as_sql_param_refs(&typed_params);
843
844 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
845 .await
846 .map(Arc::new)
847 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
848 }
849
850 async fn execute_with_projection_arc_with_session(
851 &self,
852 request: &ProjectionRequest<'_>,
853 session_vars: &[(&str, &str)],
854 ) -> Result<Arc<Vec<JsonbValue>>> {
855 if session_vars.is_empty() {
856 return self.execute_with_projection_arc(request).await;
857 }
858
859 let Some(projection) = request.projection else {
862 return self
863 .execute_where_query_arc_with_session(
864 request.view,
865 request.where_clause,
866 request.limit,
867 request.offset,
868 request.order_by,
869 session_vars,
870 )
871 .await;
872 };
873
874 let (sql, typed_params) = build_projection_select_sql(
875 projection,
876 request.view,
877 request.where_clause,
878 request.limit,
879 request.offset,
880 request.order_by,
881 )?;
882 let param_refs = crate::types::as_sql_param_refs(&typed_params);
883
884 self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
885 .await
886 .map(Arc::new)
887 }
888
889 async fn explain_query(
890 &self,
891 sql: &str,
892 _params: &[serde_json::Value],
893 ) -> Result<serde_json::Value> {
894 if sql.contains(';') {
898 return Err(FraiseQLError::Validation {
899 message: "EXPLAIN SQL must be a single statement".into(),
900 path: None,
901 });
902 }
903 let explain_sql = format!("EXPLAIN (ANALYZE false, FORMAT JSON) {sql}");
904 let client = self.acquire_connection_with_retry().await?;
905 let rows: Vec<Row> =
906 client
907 .query(explain_sql.as_str(), &[])
908 .await
909 .map_err(|e| FraiseQLError::Database {
910 message: format!("EXPLAIN failed: {e}"),
911 sql_state: e.code().map(|c| c.code().to_string()),
912 })?;
913
914 if let Some(row) = rows.first() {
915 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
916 message: format!("Failed to parse EXPLAIN output: {e}"),
917 sql_state: None,
918 })?;
919 Ok(plan)
920 } else {
921 Ok(serde_json::Value::Null)
922 }
923 }
924
925 async fn query_stats(&self, limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
926 self.pg_query_stats(limit).await
927 }
928
929 async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
930 self.pg_query_stats_by_id(id).await
931 }
932
933 async fn reset_query_stats(&self) -> Result<()> {
934 self.pg_reset_query_stats().await
935 }
936}
937
938impl SupportsMutations for PostgresAdapter {}