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