Skip to main content

cratestack_sqlx/query/read/
find_unique.rs

1//! `find_unique` — single-row read by PK with optional `FOR UPDATE`
2//! and a List/Detail policy-slot toggle.
3
4use cratestack_core::{CoolContext, CoolError};
5use cratestack_sql::ReadSource;
6
7use crate::query::support::{ReadPolicyKind, push_scoped_conditions};
8use crate::render::render_read_policy_sql;
9use crate::{SqlxRuntime, sqlx};
10
11#[derive(Clone)]
12pub struct FindUnique<'a, M: 'static, PK: 'static> {
13    pub(crate) runtime: &'a SqlxRuntime,
14    pub(crate) descriptor: &'static dyn ReadSource<M, PK>,
15    pub(crate) id: PK,
16    pub(crate) for_update: bool,
17    pub(crate) policy_kind: ReadPolicyKind,
18}
19
20impl<'a, M: 'static, PK: 'static> FindUnique<'a, M, PK> {
21    /// Emit `SELECT ... FOR UPDATE`. See `FindMany::for_update` for
22    /// the tx-pairing caveat.
23    pub fn for_update(mut self) -> Self {
24        self.for_update = true;
25        self
26    }
27
28    /// Evaluate against the schema's `detail` policy slice (the
29    /// default for `find_unique`). A no-op when called explicitly,
30    /// kept for API symmetry with [`Self::as_list`].
31    pub fn as_detail(mut self) -> Self {
32        self.policy_kind = ReadPolicyKind::Detail;
33        self
34    }
35
36    /// Evaluate against the schema's `read`/`list` policy slice
37    /// instead of `detail`. Use when the call site needs list-style
38    /// permission semantics on a unique-key lookup.
39    pub fn as_list(mut self) -> Self {
40        self.policy_kind = ReadPolicyKind::List;
41        self
42    }
43
44    pub fn preview_sql(&self) -> String {
45        let mut sql = format!(
46            "SELECT {} FROM {} WHERE {} = $1 LIMIT 1",
47            self.descriptor.select_projection(),
48            self.descriptor.table_name(),
49            self.descriptor.primary_key(),
50        );
51        if self.for_update {
52            sql.push_str(" FOR UPDATE");
53        }
54        sql
55    }
56
57    pub fn preview_scoped_sql(&self, ctx: &CoolContext) -> String {
58        let mut sql = format!(
59            "SELECT {} FROM {}",
60            self.descriptor.select_projection(),
61            self.descriptor.table_name(),
62        );
63        let mut bind_index = 1usize;
64        let (allow, deny) = match self.policy_kind {
65            ReadPolicyKind::List => (
66                self.descriptor.read_allow_policies(),
67                self.descriptor.read_deny_policies(),
68            ),
69            ReadPolicyKind::Detail => (
70                self.descriptor.detail_allow_policies(),
71                self.descriptor.detail_deny_policies(),
72            ),
73        };
74        if let Some(policy_clause) = render_read_policy_sql(allow, deny, ctx, &mut bind_index) {
75            sql.push_str(&format!(
76                " WHERE ({policy_clause}) AND {} = ${bind_index} LIMIT 1",
77                self.descriptor.primary_key()
78            ));
79        } else {
80            sql.push_str(&format!(
81                " WHERE {} = ${bind_index} LIMIT 1",
82                self.descriptor.primary_key()
83            ));
84        }
85        if self.for_update {
86            sql.push_str(" FOR UPDATE");
87        }
88        sql
89    }
90
91    pub async fn run(self, ctx: &CoolContext) -> Result<Option<M>, CoolError>
92    where
93        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow>,
94        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
95    {
96        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
97        query
98            .push(self.descriptor.select_projection())
99            .push(" FROM ")
100            .push(self.descriptor.table_name());
101        push_scoped_conditions(
102            &mut query,
103            self.descriptor,
104            &[],
105            Some((self.descriptor.primary_key(), self.id)),
106            ctx,
107            self.policy_kind,
108        );
109        query.push(" LIMIT 1");
110        if self.for_update {
111            query.push(" FOR UPDATE");
112        }
113
114        query
115            .build_query_as::<M>()
116            .fetch_optional(self.runtime.pool())
117            .await
118            .map_err(|error| CoolError::Database(error.to_string()))
119    }
120
121    pub async fn run_in_tx<'tx>(
122        self,
123        tx: &mut sqlx::Transaction<'tx, sqlx::Postgres>,
124        ctx: &CoolContext,
125    ) -> Result<Option<M>, CoolError>
126    where
127        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow>,
128        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
129    {
130        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
131        query
132            .push(self.descriptor.select_projection())
133            .push(" FROM ")
134            .push(self.descriptor.table_name());
135        push_scoped_conditions(
136            &mut query,
137            self.descriptor,
138            &[],
139            Some((self.descriptor.primary_key(), self.id)),
140            ctx,
141            self.policy_kind,
142        );
143        query.push(" LIMIT 1");
144        if self.for_update {
145            query.push(" FOR UPDATE");
146        }
147
148        query
149            .build_query_as::<M>()
150            .fetch_optional(&mut **tx)
151            .await
152            .map_err(|error| CoolError::Database(error.to_string()))
153    }
154}