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::{CratestackContext, CratestackError};
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: &CratestackContext) -> 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: &CratestackContext) -> Result<Option<M>, CratestackError>
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        // Inside an `@isolation` procedure: run on its transaction
97        // (docs/design/procedure-isolation.md §4).
98        if let Some(bound) = self.runtime.bound() {
99            return crate::bound::in_bound_savepoint!(bound, |sp| self.run_in_tx(sp, ctx));
100        }
101        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
102        query
103            .push(self.descriptor.select_projection())
104            .push(" FROM ")
105            .push(self.descriptor.table_name());
106        push_scoped_conditions(
107            &mut query,
108            self.descriptor,
109            &[],
110            Some((self.descriptor.primary_key(), self.id)),
111            ctx,
112            self.policy_kind,
113        );
114        query.push(" LIMIT 1");
115        if self.for_update {
116            query.push(" FOR UPDATE");
117        }
118
119        query
120            .build_query_as::<M>()
121            .fetch_optional(self.runtime.pool())
122            .await
123            .map_err(crate::error::cratestack_error_from_sqlx)
124    }
125
126    pub async fn run_in_tx<'tx>(
127        self,
128        tx: &mut sqlx::Transaction<'tx, sqlx::Postgres>,
129        ctx: &CratestackContext,
130    ) -> Result<Option<M>, CratestackError>
131    where
132        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow>,
133        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
134    {
135        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
136        query
137            .push(self.descriptor.select_projection())
138            .push(" FROM ")
139            .push(self.descriptor.table_name());
140        push_scoped_conditions(
141            &mut query,
142            self.descriptor,
143            &[],
144            Some((self.descriptor.primary_key(), self.id)),
145            ctx,
146            self.policy_kind,
147        );
148        query.push(" LIMIT 1");
149        if self.for_update {
150            query.push(" FOR UPDATE");
151        }
152
153        query
154            .build_query_as::<M>()
155            .fetch_optional(&mut **tx)
156            .await
157            .map_err(crate::error::cratestack_error_from_sqlx)
158    }
159}