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, composed::build_composed_select_sql, jsonb_cell,
16 numeric::PgNumericText,
17};
18use crate::{
19 identifier::quote_postgres_identifier,
20 postgres::pg_detail,
21 traits::{
22 ColumnRowStream, ComposedLevel, DatabaseAdapter, JsonbRowStream, ProjectionRequest, Writer,
23 },
24 types::{
25 DatabaseType, JsonbValue, PoolMetrics, QueryParam, ReadRouting,
26 sql_hints::{OrderByClause, SqlProjectionHint},
27 },
28 where_clause::WhereClause,
29};
30
31async fn run_function_in_txn(
42 adapter: &PostgresAdapter,
43 request: &crate::traits::WriteRequest<'_>,
44 gate: crate::traits::MutationRowGate<'_>,
45) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
46 let crate::traits::WriteRequest {
47 function: function_name,
48 args,
49 session_vars,
50 changelog,
51 mode,
52 ..
53 } = *request;
54 let commit = matches!(mode, crate::traits::WriteMode::Commit);
55 if commit {
58 adapter.mark_write();
59 }
60 let quoted_fn = quote_postgres_identifier(function_name);
61
62 let sql = if let Some(cl) = changelog {
66 build_changelog_cte_sql("ed_fn, args.len(), cl.pre_image)
67 } else {
68 let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
69 format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "))
70 };
71
72 let mut flex_args: Vec<FlexParam> = args
77 .iter()
78 .map(|v| match v {
79 serde_json::Value::Null => FlexParam::Null,
80 serde_json::Value::String(s) => FlexParam::Text(s.clone()),
81 _ => FlexParam::Text(v.to_string()),
82 })
83 .collect();
84 if let Some(changelog) = changelog {
85 flex_args.push(FlexParam::Text(changelog.object_type.to_string()));
95 flex_args.push(FlexParam::Text(changelog.modification_type.to_string()));
96 flex_args
99 .push(changelog.tenant_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
100 flex_args
102 .push(changelog.trace_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
103 flex_args.push(
105 changelog
106 .schema_version
107 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
108 );
109 flex_args.push(
111 changelog
112 .trace_context
113 .map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
114 );
115 flex_args
117 .push(changelog.actor_type.map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())));
118 flex_args
121 .push(changelog.acting_for.map_or(FlexParam::Null, |u| FlexParam::Text(u.to_string())));
122 flex_args
125 .push(changelog.transport.map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())));
126 }
127 let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
128 .iter()
129 .map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
130 .collect();
131
132 let mut client = adapter.acquire_connection_with_retry().await?;
133 let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
136 let mechanism = if changelog.is_some() {
140 " (with change-log outbox)"
141 } else {
142 ""
143 };
144 let txn = client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
145 message: format!("Failed to start mutation{mechanism} transaction: {}", pg_detail(&e)),
146 sql_state: e.code().map(|c| c.code().to_string()),
147 })?;
148
149 apply_session_vars(&txn, session_vars).await?;
152
153 mark_cdc_mediated(&txn).await?;
158
159 if adapter.mutation_timing_enabled {
161 txn.execute(
162 "SELECT set_config($1, clock_timestamp()::text, true)",
163 &[&adapter.timing_variable_name],
164 )
165 .await
166 .map_err(|e| FraiseQLError::Database {
167 message: format!("Failed to set mutation timing variable: {}", pg_detail(&e)),
168 sql_state: e.code().map(|c| c.code().to_string()),
169 })?;
170 }
171
172 if changelog.is_some() {
181 let started_at_set =
182 session_vars.iter().any(|(name, _)| *name == crate::changelog::STARTED_AT_VAR)
183 || (adapter.mutation_timing_enabled
184 && adapter.timing_variable_name == crate::changelog::STARTED_AT_VAR);
185 if !started_at_set {
186 txn.execute(
187 "SELECT set_config($1, clock_timestamp()::text, true)",
188 &[&crate::changelog::STARTED_AT_VAR],
189 )
190 .await
191 .map_err(|e| FraiseQLError::Database {
192 message: format!("Failed to stamp change-log started_at: {}", pg_detail(&e)),
193 sql_state: e.code().map(|c| c.code().to_string()),
194 })?;
195 }
196 }
197
198 let rows: Vec<Row> =
199 txn.query(&stmt, params.as_slice()).await.map_err(|e| FraiseQLError::Database {
200 message: format!(
201 "Function call {function_name}{mechanism} failed: {}",
202 pg_detail(&e)
203 ),
204 sql_state: e.code().map(|c| c.code().to_string()),
205 })?;
206
207 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
208 rows.iter().map(row_to_map).collect();
209
210 if let Err(refusal) = gate(&results) {
215 txn.rollback().await.map_err(|e| FraiseQLError::Database {
216 message: format!(
217 "Failed to roll back refused mutation transaction: {}",
218 pg_detail(&e)
219 ),
220 sql_state: e.code().map(|c| c.code().to_string()),
221 })?;
222 return Err(refusal);
223 }
224
225 if !commit {
226 txn.rollback().await.map_err(|e| FraiseQLError::Database {
228 message: format!("Failed to roll back dry-run transaction: {}", pg_detail(&e)),
229 sql_state: e.code().map(|c| c.code().to_string()),
230 })?;
231 return Ok(results);
232 }
233
234 txn.commit().await.map_err(|e| FraiseQLError::Database {
235 message: format!("Failed to commit mutation{mechanism} transaction: {}", pg_detail(&e)),
236 sql_state: e.code().map(|c| c.code().to_string()),
237 })?;
238 adapter.mark_write();
239
240 Ok(results)
241}
242
243const PG_UNDEFINED_COLUMN: &str = "42703";
245
246#[derive(Debug)]
261enum FlexParam {
262 Null,
264 Text(String),
266}
267
268impl tokio_postgres::types::ToSql for FlexParam {
269 fn to_sql(
270 &self,
271 ty: &tokio_postgres::types::Type,
272 out: &mut bytes::BytesMut,
273 ) -> std::result::Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>>
274 {
275 use tokio_postgres::types::{IsNull, Type};
276 match self {
277 Self::Null => Ok(IsNull::Yes),
278 Self::Text(s) => {
279 if *ty == Type::JSONB {
280 out.put_u8(1);
282 out.extend_from_slice(s.as_bytes());
283 } else if *ty == Type::JSON {
284 out.extend_from_slice(s.as_bytes());
285 } else if *ty == Type::UUID {
286 let uuid = uuid::Uuid::parse_str(s)?;
287 out.extend_from_slice(uuid.as_bytes());
288 } else if *ty == Type::INT4 {
289 let n: i32 = s.parse()?;
290 out.put_i32(n);
291 } else if *ty == Type::INT8 {
292 let n: i64 = s.parse()?;
293 out.put_i64(n);
294 } else if *ty == Type::BOOL {
295 let b: bool = s.parse()?;
296 out.put_u8(u8::from(b));
297 } else {
298 out.extend_from_slice(s.as_bytes());
301 }
302 Ok(IsNull::No)
303 },
304 }
305 }
306
307 fn accepts(_ty: &tokio_postgres::types::Type) -> bool {
308 true
310 }
311
312 fn to_sql_checked(
313 &self,
314 ty: &tokio_postgres::types::Type,
315 out: &mut bytes::BytesMut,
316 ) -> std::result::Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>>
317 {
318 self.to_sql(ty, out)
321 }
322}
323
324fn enrich_undefined_column_error(
332 err: FraiseQLError,
333 view: &str,
334 where_clause: Option<&WhereClause>,
335) -> FraiseQLError {
336 let FraiseQLError::Database { ref sql_state, .. } = err else {
337 return err;
338 };
339 if sql_state.as_deref() != Some(PG_UNDEFINED_COLUMN) {
340 return err;
341 }
342 let native_cols: Vec<&str> =
343 where_clause.map(|wc| wc.native_column_names()).unwrap_or_default();
344 if native_cols.is_empty() {
345 return err;
346 }
347 FraiseQLError::Database {
348 message: format!(
349 "Column(s) {:?} referenced as native column(s) on `{view}` do not exist. \
350 These columns were auto-inferred from ID/UUID-typed query arguments. \
351 Either add the column(s) to the table/view, or set \
352 `native_columns = {{}}` explicitly in your schema to disable inference.",
353 native_cols,
354 ),
355 sql_state: Some(PG_UNDEFINED_COLUMN.to_string()),
356 }
357}
358
359struct PgEnumLabel(String);
366
367impl<'a> FromSql<'a> for PgEnumLabel {
368 fn from_sql(
369 _ty: &Type,
370 raw: &'a [u8],
371 ) -> std::result::Result<Self, Box<dyn std::error::Error + Sync + Send>> {
372 Ok(Self(std::str::from_utf8(raw)?.to_owned()))
373 }
374
375 fn accepts(ty: &Type) -> bool {
376 matches!(ty.kind(), Kind::Enum(_))
377 }
378}
379
380fn row_to_map(row: &Row) -> std::collections::HashMap<String, serde_json::Value> {
385 let mut map = std::collections::HashMap::new();
386 for (idx, column) in row.columns().iter().enumerate() {
387 map.insert(column.name().to_string(), decode_cell(row, idx));
388 }
389 map
390}
391
392pub(super) fn decode_cell(row: &Row, idx: usize) -> serde_json::Value {
403 let column = &row.columns()[idx];
404 if let Ok(v) = row.try_get::<_, i16>(idx) {
405 serde_json::json!(v)
412 } else if let Ok(v) = row.try_get::<_, i32>(idx) {
413 serde_json::json!(v)
414 } else if let Ok(v) = row.try_get::<_, i64>(idx) {
415 serde_json::json!(v)
416 } else if let Ok(v) = row.try_get::<_, f64>(idx) {
417 serde_json::json!(v)
418 } else if let Ok(v) = row.try_get::<_, String>(idx) {
419 serde_json::json!(v)
420 } else if let Ok(v) = row.try_get::<_, bool>(idx) {
421 serde_json::json!(v)
422 } else if let Ok(v) = row.try_get::<_, PgNumericText>(idx) {
423 let s = v.0;
430 serde_json::from_str::<serde_json::Value>(&s).unwrap_or(serde_json::Value::String(s))
431 } else if let Ok(v) = row.try_get::<_, uuid::Uuid>(idx) {
432 serde_json::json!(v.to_string())
435 } else if let Ok(v) = row.try_get::<_, chrono::DateTime<chrono::Utc>>(idx) {
436 serde_json::json!(v.to_rfc3339())
438 } else if let Ok(v) = row.try_get::<_, chrono::NaiveDateTime>(idx) {
439 serde_json::json!(v.to_string())
441 } else if let Ok(v) = row.try_get::<_, chrono::NaiveDate>(idx) {
442 serde_json::json!(v.to_string())
444 } else if let Ok(v) = row.try_get::<_, Vec<String>>(idx) {
445 serde_json::json!(v)
450 } else if let Ok(v) = row.try_get::<_, PgEnumLabel>(idx) {
451 serde_json::json!(v.0)
460 } else if let Ok(v) = row.try_get::<_, serde_json::Value>(idx) {
461 v
462 } else {
463 tracing::warn!(
467 column = %column.name(),
468 pg_type = %column.type_(),
469 "decode_cell: column type not representable as JSON; returning null"
470 );
471 serde_json::Value::Null
472 }
473}
474
475pub(super) async fn apply_session_vars(
481 txn: &tokio_postgres::Transaction<'_>,
482 session_vars: &[(&str, &str)],
483) -> Result<()> {
484 for (name, value) in session_vars {
485 if *value == crate::changelog::CLOCK_TIMESTAMP_DIRECTIVE {
490 txn.execute("SELECT set_config($1, clock_timestamp()::text, true)", &[name])
491 .await
492 .map_err(|e| FraiseQLError::Database {
493 message: format!(
494 "set_config({name:?}, clock_timestamp()) failed: {}",
495 pg_detail(&e)
496 ),
497 sql_state: e.code().map(|c| c.code().to_string()),
498 })?;
499 } else {
500 txn.execute("SELECT set_config($1, $2, true)", &[name, value])
501 .await
502 .map_err(|e| FraiseQLError::Database {
503 message: format!("set_config({name:?}) failed: {}", pg_detail(&e)),
504 sql_state: e.code().map(|c| c.code().to_string()),
505 })?;
506 }
507 }
508 Ok(())
509}
510
511pub(super) async fn mark_cdc_mediated(txn: &tokio_postgres::Transaction<'_>) -> Result<()> {
523 txn.execute(
524 "SELECT set_config($1, $2, true)",
525 &[
526 &crate::changelog::CDC_MEDIATED_VAR,
527 &crate::changelog::CDC_MEDIATED_ON,
528 ],
529 )
530 .await
531 .map_err(|e| FraiseQLError::Database {
532 message: format!(
533 "Failed to set {} marker: {}",
534 crate::changelog::CDC_MEDIATED_VAR,
535 pg_detail(&e)
536 ),
537 sql_state: e.code().map(|c| c.code().to_string()),
538 })?;
539 Ok(())
540}
541
542pub(super) fn build_changelog_cte_sql(quoted_fn: &str, n_args: usize, pre_image: bool) -> String {
574 let placeholders: Vec<String> = (1..=n_args).map(|i| format!("${i}")).collect();
575 let object_type_idx = n_args + 1;
576 let modification_type_idx = n_args + 2;
577 let tenant_id_idx = n_args + 3;
578 let trace_id_idx = n_args + 4;
579 let schema_version_idx = n_args + 5;
580 let trace_context_idx = n_args + 6;
581 let actor_type_idx = n_args + 7;
582 let acting_for_idx = n_args + 8;
583 let transport_idx = n_args + 9;
584 let started_var = crate::changelog::STARTED_AT_VAR;
585 let duration = crate::changelog::duration_ms_sql(started_var);
586 let calc_version = crate::changelog::DURATION_CALC_VERSION;
587 let after_before_cols = if pre_image {
596 "object_data, object_data_before"
597 } else {
598 "object_data"
599 };
600 let after_before_vals = if pre_image {
601 "r.entity, r.entity_before"
602 } else {
603 "r.entity"
604 };
605 format!(
606 "WITH r AS MATERIALIZED (SELECT * FROM {quoted_fn}({args})), \
607 _changelog AS ( \
608 INSERT INTO core.tb_entity_change_log \
609 (object_type, modification_type, object_id, {after_before_cols}, \
610 updated_fields, cascade, started_at, duration_ms, extra_metadata, \
611 tenant_id, trace_id, schema_version, trace_context, \
612 actor_type, acting_for, commit_time) \
613 SELECT \
614 COALESCE(r.entity_type, ${object_type_idx}), \
615 ${modification_type_idx}, \
616 r.entity_id, {after_before_vals}, r.updated_fields, r.cascade, \
617 current_setting('{started_var}')::timestamptz, \
618 {duration}, \
619 jsonb_build_object('duration_calc_version', {calc_version}::int) \
620 || CASE WHEN ${transport_idx}::text IS NULL THEN '{{}}'::jsonb \
621 ELSE jsonb_build_object('transport', ${transport_idx}::text) END, \
622 ${tenant_id_idx}::uuid, \
623 ${trace_id_idx}, \
624 ${schema_version_idx}, \
625 ${trace_context_idx}::jsonb, \
626 ${actor_type_idx}, \
627 ${acting_for_idx}::uuid, \
628 clock_timestamp() \
629 FROM r \
630 WHERE r.succeeded AND r.state_changed \
631 RETURNING 1 \
632 ) \
633 SELECT * FROM r",
634 args = placeholders.join(", "),
635 )
636}
637
638async fn prepare_cached_stmt(
650 client: &deadpool_postgres::Client,
651 sql: &str,
652) -> Result<tokio_postgres::Statement> {
653 client.prepare_cached(sql).await.map_err(|e| {
654 let detail = pg_detail(&e);
658 let hint = changelog_prepare_hint(&detail);
659 FraiseQLError::Database {
660 message: format!("Failed to prepare statement: {detail}{hint}"),
661 sql_state: e.code().map(|c| c.code().to_string()),
662 }
663 })
664}
665
666fn changelog_prepare_hint(detail: &str) -> &'static str {
676 if detail.contains("core.tb_entity_change_log") {
677 "\nhint: the mutation change-log outbox table is not installed in this database — \
678 run `fraiseql setup` (or apply the fraiseql-observers change-log contract \
679 migration) before executing mutations"
680 } else if detail.contains("column r.") {
681 "\nhint: the mutation function does not return the v2.2 mutation_response row \
682 (13 columns: succeeded, state_changed, …) that the change-log outbox reads. \
683 `RETURNS SETOF v_*` and bare scalar returns are not supported — build the row \
684 with fraiseql.mutation_ok()/fraiseql.mutation_err() (installed by `fraiseql \
685 setup`); see docs/architecture/mutation-response.md"
686 } else {
687 ""
688 }
689}
690
691#[async_trait]
695impl DatabaseAdapter for PostgresAdapter {
696 async fn execute_with_projection(
697 &self,
698 view: &str,
699 projection: Option<&SqlProjectionHint>,
700 where_clause: Option<&WhereClause>,
701 limit: Option<u32>,
702 offset: Option<u32>,
703 order_by: Option<&[OrderByClause]>,
704 ) -> Result<Vec<JsonbValue>> {
705 self.execute_with_projection_impl(view, projection, where_clause, limit, offset, order_by)
706 .await
707 }
708
709 async fn execute_where_query(
710 &self,
711 view: &str,
712 where_clause: Option<&WhereClause>,
713 limit: Option<u32>,
714 offset: Option<u32>,
715 order_by: Option<&[OrderByClause]>,
716 ) -> Result<Vec<JsonbValue>> {
717 let (sql, typed_params) =
718 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
719
720 let param_refs = crate::types::as_sql_param_refs(&typed_params);
721
722 self.execute_raw(&sql, ¶m_refs, ReadRouting::Any)
723 .await
724 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
725 }
726
727 async fn explain_where_query(
728 &self,
729 view: &str,
730 where_clause: Option<&WhereClause>,
731 limit: Option<u32>,
732 offset: Option<u32>,
733 ) -> Result<serde_json::Value> {
734 let (select_sql, typed_params) = build_where_select_sql(view, where_clause, limit, offset)?;
735 if select_sql.contains(';') {
738 return Err(FraiseQLError::Validation {
739 message: "EXPLAIN SQL must be a single statement".into(),
740 path: None,
741 });
742 }
743 let explain_sql = format!("EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) {select_sql}");
744
745 let param_refs = crate::types::as_sql_param_refs(&typed_params);
746
747 let client = self.acquire_read_connection_with_retry(ReadRouting::Any).await?;
750 let rows = client.query(explain_sql.as_str(), ¶m_refs).await.map_err(|e| {
751 FraiseQLError::Database {
752 message: format!("EXPLAIN ANALYZE failed: {}", pg_detail(&e)),
753 sql_state: e.code().map(|c| c.code().to_string()),
754 }
755 })?;
756
757 if let Some(row) = rows.first() {
758 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
759 message: format!("Failed to parse EXPLAIN output: {e}"),
760 sql_state: None,
761 })?;
762 Ok(plan)
763 } else {
764 Ok(serde_json::Value::Null)
765 }
766 }
767
768 fn database_type(&self) -> DatabaseType {
769 DatabaseType::PostgreSQL
770 }
771
772 fn supports_composed_reads(&self) -> bool {
774 true
775 }
776
777 async fn health_check(&self) -> Result<()> {
778 let client = self.acquire_connection_with_retry().await?;
780
781 client.query("SELECT 1", &[]).await.map_err(|e| FraiseQLError::Database {
782 message: format!("Health check failed: {}", pg_detail(&e)),
783 sql_state: e.code().map(|c| c.code().to_string()),
784 })?;
785
786 Ok(())
787 }
788
789 #[allow(clippy::cast_possible_truncation)] fn pool_metrics(&self) -> PoolMetrics {
791 let status = self.pool.status();
792
793 PoolMetrics {
794 total_connections: status.size as u32,
795 idle_connections: status.available as u32,
796 active_connections: (status.size - status.available) as u32,
797 waiting_requests: status.waiting as u32,
798 }
799 }
800
801 async fn execute_raw_query(
806 &self,
807 sql: &str,
808 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
809 let client = self.acquire_connection_with_retry().await?;
811
812 let rows: Vec<Row> = client.query(sql, &[]).await.map_err(|e| FraiseQLError::Database {
813 message: format!("Query execution failed: {}", pg_detail(&e)),
814 sql_state: e.code().map(|c| c.code().to_string()),
815 })?;
816
817 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
819 rows.iter().map(row_to_map).collect();
820
821 Ok(results)
822 }
823
824 async fn execute_parameterized_aggregate(
825 &self,
826 sql: &str,
827 params: &[serde_json::Value],
828 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
829 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
833 let param_refs = crate::types::as_sql_param_refs(&typed);
834
835 let client = self.acquire_read_connection_with_retry(ReadRouting::Any).await?;
837 let rows: Vec<Row> =
838 client.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
839 message: format!("Parameterized aggregate query failed: {}", pg_detail(&e)),
840 sql_state: e.code().map(|c| c.code().to_string()),
841 })?;
842
843 let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
844 rows.iter().map(row_to_map).collect();
845
846 Ok(results)
847 }
848
849 async fn execute_parameterized_aggregate_with_session(
850 &self,
851 sql: &str,
852 params: &[serde_json::Value],
853 session_vars: &[(&str, &str)],
854 routing: ReadRouting,
855 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
856 if session_vars.is_empty() {
857 return self.execute_parameterized_aggregate(sql, params).await;
858 }
859
860 let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
861 let param_refs = crate::types::as_sql_param_refs(&typed);
862
863 let mut client = self.acquire_read_connection_with_retry(routing).await?;
865 let txn =
866 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
867 message: format!("Failed to start session-var transaction: {}", pg_detail(&e)),
868 sql_state: e.code().map(|c| c.code().to_string()),
869 })?;
870 apply_session_vars(&txn, session_vars).await?;
871 let rows: Vec<Row> =
872 txn.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
873 message: format!("Parameterized aggregate query failed: {}", pg_detail(&e)),
874 sql_state: e.code().map(|c| c.code().to_string()),
875 })?;
876 txn.commit().await.map_err(|e| FraiseQLError::Database {
877 message: format!("Failed to commit session-var transaction: {}", pg_detail(&e)),
878 sql_state: e.code().map(|c| c.code().to_string()),
879 })?;
880
881 Ok(rows.iter().map(row_to_map).collect())
882 }
883
884 async fn execute_where_query_arc_with_session(
890 &self,
891 view: &str,
892 where_clause: Option<&WhereClause>,
893 limit: Option<u32>,
894 offset: Option<u32>,
895 order_by: Option<&[OrderByClause]>,
896 session_vars: &[(&str, &str)],
897 routing: ReadRouting,
898 ) -> Result<Arc<Vec<JsonbValue>>> {
899 let (sql, typed_params) =
900 build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
901 let param_refs = crate::types::as_sql_param_refs(&typed_params);
902
903 if session_vars.is_empty() {
909 return self
910 .execute_raw(&sql, ¶m_refs, routing)
911 .await
912 .map(Arc::new)
913 .map_err(|e| enrich_undefined_column_error(e, view, where_clause));
914 }
915
916 self.execute_raw_with_session(&sql, ¶m_refs, session_vars, routing)
917 .await
918 .map(Arc::new)
919 .map_err(|e| enrich_undefined_column_error(e, view, where_clause))
920 }
921
922 async fn count_where_query(
928 &self,
929 view: &str,
930 where_clause: Option<&WhereClause>,
931 session_vars: &[(&str, &str)],
932 routing: ReadRouting,
933 ) -> Result<u64> {
934 let (sql, typed_params) = super::build_count_sql(view, where_clause)?;
935 let param_refs = crate::types::as_sql_param_refs(&typed_params);
936
937 let row = if session_vars.is_empty() {
940 let client = self.acquire_read_connection_with_retry(routing).await?;
941 client.query_one(&sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
942 message: format!("Count query failed: {}", pg_detail(&e)),
943 sql_state: e.code().map(|c| c.code().to_string()),
944 })?
945 } else {
946 let mut client = self.acquire_read_connection_with_retry(routing).await?;
949 let txn =
950 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
951 message: format!(
952 "Failed to start count session-var transaction: {}",
953 pg_detail(&e)
954 ),
955 sql_state: e.code().map(|c| c.code().to_string()),
956 })?;
957 super::database::apply_session_vars(&txn, session_vars).await?;
958 let row =
959 txn.query_one(&sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
960 message: format!("Count query failed: {}", pg_detail(&e)),
961 sql_state: e.code().map(|c| c.code().to_string()),
962 })?;
963 txn.commit().await.map_err(|e| FraiseQLError::Database {
964 message: format!(
965 "Failed to commit count session-var transaction: {}",
966 pg_detail(&e)
967 ),
968 sql_state: e.code().map(|c| c.code().to_string()),
969 })?;
970 row
971 };
972
973 let total: i64 = row.get(0);
974 Ok(total.cast_unsigned())
977 }
978
979 async fn execute_with_projection_arc_with_session(
980 &self,
981 request: &ProjectionRequest<'_>,
982 session_vars: &[(&str, &str)],
983 routing: ReadRouting,
984 ) -> Result<Arc<Vec<JsonbValue>>> {
985 let Some(projection) = request.projection else {
988 return self
989 .execute_where_query_arc_with_session(
990 request.view,
991 request.where_clause,
992 request.limit,
993 request.offset,
994 request.order_by,
995 session_vars,
996 routing,
997 )
998 .await;
999 };
1000
1001 let (sql, typed_params) = build_projection_select_sql(
1002 projection,
1003 request.view,
1004 request.where_clause,
1005 request.limit,
1006 request.offset,
1007 request.order_by,
1008 )?;
1009 let param_refs = crate::types::as_sql_param_refs(&typed_params);
1010
1011 if session_vars.is_empty() {
1014 return self.execute_raw(&sql, ¶m_refs, routing).await.map(Arc::new);
1015 }
1016
1017 self.execute_raw_with_session(&sql, ¶m_refs, session_vars, routing)
1018 .await
1019 .map(Arc::new)
1020 }
1021
1022 async fn execute_composed_with_session(
1029 &self,
1030 read: &ComposedLevel,
1031 session_vars: &[(&str, &str)],
1032 routing: ReadRouting,
1033 ) -> Result<Arc<Vec<JsonbValue>>> {
1034 let (sql, typed_params) = build_composed_select_sql(read)?;
1035 let param_refs = crate::types::as_sql_param_refs(&typed_params);
1036
1037 if session_vars.is_empty() {
1040 return self.execute_raw(&sql, ¶m_refs, routing).await.map(Arc::new);
1041 }
1042
1043 self.execute_raw_with_session(&sql, ¶m_refs, session_vars, routing)
1044 .await
1045 .map(Arc::new)
1046 }
1047
1048 async fn stream_with_projection(
1049 &self,
1050 request: &ProjectionRequest<'_>,
1051 session_vars: &[(&str, &str)],
1052 routing: ReadRouting,
1053 ) -> Result<JsonbRowStream> {
1054 let (sql, typed_params) = match request.projection {
1055 Some(projection) => build_projection_select_sql(
1056 projection,
1057 request.view,
1058 request.where_clause,
1059 request.limit,
1060 request.offset,
1061 request.order_by,
1062 )?,
1063 None => build_where_select_sql_ordered(
1064 request.view,
1065 request.where_clause,
1066 request.limit,
1067 request.offset,
1068 request.order_by,
1069 )?,
1070 };
1071
1072 super::streaming::stream_rows(self, sql, typed_params, session_vars, routing, |row, sql| {
1073 jsonb_cell(row, 0, sql)
1074 })
1075 .await
1076 .map_err(|e| enrich_undefined_column_error(e, request.view, request.where_clause))
1077 }
1078
1079 async fn stream_row_query(
1085 &self,
1086 view_name: &str,
1087 columns: &[crate::types::ColumnSpec],
1088 where_sql: Option<&str>,
1089 order_by: Option<&str>,
1090 limit: Option<u32>,
1091 offset: Option<u32>,
1092 ) -> Result<ColumnRowStream> {
1093 let sql = crate::traits::build_row_query_sql(view_name, where_sql, order_by, limit, offset);
1094 let columns = columns.to_vec();
1095
1096 super::streaming::stream_rows(
1097 self,
1098 sql,
1099 Vec::new(),
1100 &[],
1101 ReadRouting::Primary,
1102 move |row, _sql| Ok(crate::traits::row_to_column_values(&row_to_map(row), &columns)),
1103 )
1104 .await
1105 }
1106
1107 async fn execute_row_query_with_session(
1114 &self,
1115 view_name: &str,
1116 columns: &[crate::types::ColumnSpec],
1117 where_sql: Option<&str>,
1118 order_by: Option<&str>,
1119 limit: Option<u32>,
1120 offset: Option<u32>,
1121 session_vars: &[(&str, &str)],
1122 ) -> Result<Vec<Vec<crate::types::ColumnValue>>> {
1123 if session_vars.is_empty() {
1124 return self
1125 .execute_row_query(view_name, columns, where_sql, order_by, limit, offset)
1126 .await;
1127 }
1128
1129 let sql = crate::traits::build_row_query_sql(view_name, where_sql, order_by, limit, offset);
1130
1131 let mut client = self.acquire_connection_with_retry().await?;
1132 let txn =
1133 client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
1134 message: format!(
1135 "Failed to start row-read session-var transaction: {}",
1136 pg_detail(&e)
1137 ),
1138 sql_state: e.code().map(|c| c.code().to_string()),
1139 })?;
1140 apply_session_vars(&txn, session_vars).await?;
1141 let rows: Vec<Row> =
1142 txn.query(sql.as_str(), &[]).await.map_err(|e| FraiseQLError::Database {
1143 message: format!("Row query execution failed: {}", pg_detail(&e)),
1144 sql_state: e.code().map(|c| c.code().to_string()),
1145 })?;
1146 txn.commit().await.map_err(|e| FraiseQLError::Database {
1147 message: format!(
1148 "Failed to commit row-read session-var transaction: {}",
1149 pg_detail(&e)
1150 ),
1151 sql_state: e.code().map(|c| c.code().to_string()),
1152 })?;
1153
1154 Ok(rows
1155 .iter()
1156 .map(|row| crate::traits::row_to_column_values(&row_to_map(row), columns))
1157 .collect())
1158 }
1159
1160 async fn stream_row_query_with_session(
1165 &self,
1166 view_name: &str,
1167 columns: &[crate::types::ColumnSpec],
1168 where_sql: Option<&str>,
1169 order_by: Option<&str>,
1170 limit: Option<u32>,
1171 offset: Option<u32>,
1172 session_vars: &[(&str, &str)],
1173 ) -> Result<ColumnRowStream> {
1174 let sql = crate::traits::build_row_query_sql(view_name, where_sql, order_by, limit, offset);
1175 let columns = columns.to_vec();
1176
1177 super::streaming::stream_rows(
1178 self,
1179 sql,
1180 Vec::new(),
1181 session_vars,
1182 ReadRouting::Primary,
1183 move |row, _sql| Ok(crate::traits::row_to_column_values(&row_to_map(row), &columns)),
1184 )
1185 .await
1186 }
1187
1188 async fn explain_query(
1189 &self,
1190 sql: &str,
1191 _params: &[serde_json::Value],
1192 ) -> Result<serde_json::Value> {
1193 if sql.contains(';') {
1197 return Err(FraiseQLError::Validation {
1198 message: "EXPLAIN SQL must be a single statement".into(),
1199 path: None,
1200 });
1201 }
1202 let explain_sql = format!("EXPLAIN (ANALYZE false, FORMAT JSON) {sql}");
1203 let client = self.acquire_read_connection_with_retry(ReadRouting::Any).await?;
1205 let rows: Vec<Row> =
1206 client
1207 .query(explain_sql.as_str(), &[])
1208 .await
1209 .map_err(|e| FraiseQLError::Database {
1210 message: format!("EXPLAIN failed: {}", pg_detail(&e)),
1211 sql_state: e.code().map(|c| c.code().to_string()),
1212 })?;
1213
1214 if let Some(row) = rows.first() {
1215 let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
1216 message: format!("Failed to parse EXPLAIN output: {e}"),
1217 sql_state: None,
1218 })?;
1219 Ok(plan)
1220 } else {
1221 Ok(serde_json::Value::Null)
1222 }
1223 }
1224
1225 async fn query_stats(&self, limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
1226 self.pg_query_stats(limit).await
1227 }
1228
1229 async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
1230 self.pg_query_stats_by_id(id).await
1231 }
1232
1233 async fn reset_query_stats(&self) -> Result<()> {
1234 self.pg_reset_query_stats().await
1235 }
1236
1237 async fn execute_admin_sql(
1244 &self,
1245 request: &crate::traits::AdminSqlRequest,
1246 ) -> Result<crate::traits::AdminSqlOutcome> {
1247 super::admin_sql::execute_admin_sql_impl(self, request).await
1248 }
1249}
1250
1251#[async_trait]
1253impl Writer for PostgresAdapter {
1254 async fn execute_write(
1255 &self,
1256 request: &crate::traits::WriteRequest<'_>,
1257 gate: crate::traits::MutationRowGate<'_>,
1258 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
1259 run_function_in_txn(self, request, gate).await
1260 }
1261}