oxisql_postgres/prepared.rs
1//! [`PgPrepared`] — a compiled PostgreSQL prepared statement.
2//!
3//! Wraps a `tokio_postgres::Statement` (a server-side handle) together with
4//! a shared reference to the underlying client so the statement can be
5//! re-executed with different parameters without re-parsing on every call.
6//!
7//! # Cache
8//!
9//! [`StmtCache`] is a `std::sync::Mutex`-guarded `HashMap` keyed on the SQL
10//! text. Using `std::sync::Mutex` (not `tokio::sync::Mutex`) is deliberate:
11//! the lock is always acquired and released *synchronously* — it is never held
12//! across an `await` point, so no deadlock / blocking risk.
13//!
14//! `tokio_postgres::Statement` is cheaply `Clone`-able; it holds a
15//! reference-counted handle to the server-side plan.
16
17use std::collections::HashMap;
18use std::sync::{Arc, Mutex};
19
20use async_trait::async_trait;
21use tokio_postgres::Statement;
22
23use oxisql_core::{OxiSqlError, PreparedStatement, Row, ToSqlValue};
24
25use crate::types::{pg_row_to_row, value_to_param, OwnedParam};
26
27// ── StmtCache ──────────────────────────────────────────────────────────────────
28
29/// A shared, synchronously-locked cache mapping SQL text → compiled
30/// `tokio_postgres::Statement`.
31pub(crate) type StmtCache = Arc<Mutex<HashMap<String, Statement>>>;
32
33// ── helper ─────────────────────────────────────────────────────────────────────
34
35fn build_owned(params: &[&dyn ToSqlValue]) -> Vec<OwnedParam> {
36 params
37 .iter()
38 .map(|p| value_to_param(&p.to_value()))
39 .collect()
40}
41
42fn owned_refs(owned: &[OwnedParam]) -> Vec<&(dyn tokio_postgres::types::ToSql + Sync)> {
43 owned.iter().map(|p| p as _).collect()
44}
45
46// ── PgPrepared ─────────────────────────────────────────────────────────────────
47
48/// A compiled PostgreSQL prepared statement.
49///
50/// Obtain via [`oxisql_core::Connection::prepare`] on a [`crate::PgConnection`].
51///
52/// The statement is compiled once by the server and re-used for every
53/// subsequent execution, avoiding repeated parse/plan cycles.
54pub struct PgPrepared {
55 pub(crate) client: Arc<tokio::sync::Mutex<tokio_postgres::Client>>,
56 pub(crate) stmt: Statement,
57 pub(crate) sql_text: String,
58}
59
60#[async_trait]
61impl PreparedStatement for PgPrepared {
62 /// Execute the prepared statement (DML/DDL) and return the row count.
63 async fn execute(&mut self, params: &[&dyn ToSqlValue]) -> Result<u64, OxiSqlError> {
64 let owned = build_owned(params);
65 let refs = owned_refs(&owned);
66 let client = self.client.lock().await;
67 client
68 .execute(&self.stmt, refs.as_slice())
69 .await
70 .map_err(|e| OxiSqlError::Execution(e.to_string()))
71 }
72
73 /// Execute the prepared statement as a `SELECT` and return all rows.
74 async fn query(&mut self, params: &[&dyn ToSqlValue]) -> Result<Vec<Row>, OxiSqlError> {
75 let owned = build_owned(params);
76 let refs = owned_refs(&owned);
77 let client = self.client.lock().await;
78 let pg_rows = client
79 .query(&self.stmt, refs.as_slice())
80 .await
81 .map_err(|e| OxiSqlError::Execution(e.to_string()))?;
82 pg_rows
83 .into_iter()
84 .map(|r| pg_row_to_row(r).map_err(OxiSqlError::from))
85 .collect()
86 }
87
88 /// Return the original SQL text this statement was compiled from.
89 fn sql(&self) -> &str {
90 &self.sql_text
91 }
92}