Skip to main content

cratestack_sqlx/query/read/
projected_find_unique.rs

1//! `find_unique(...).select([...])` — projected single-row read that
2//! returns `Option<Projection<M>>`. Non-selected fields hold
3//! `Default::default()` values; callers gate reads on
4//! `Projection::is_selected(col)`.
5
6use cratestack_core::{CratestackContext, CratestackError};
7use cratestack_sql::{IntoColumnName, ReadSource};
8
9use crate::query::support::{ReadPolicyKind, push_scoped_conditions};
10use crate::{SqlxRuntime, sqlx};
11
12use super::find_unique::FindUnique;
13
14#[derive(Clone)]
15pub struct ProjectedFindUnique<'a, M: 'static, PK: 'static> {
16    runtime: &'a SqlxRuntime,
17    descriptor: &'static dyn ReadSource<M, PK>,
18    id: PK,
19    selected: Vec<&'static str>,
20    policy_kind: ReadPolicyKind,
21    for_update: bool,
22}
23
24impl<'a, M: 'static, PK: 'static> ProjectedFindUnique<'a, M, PK> {
25    pub fn as_detail(mut self) -> Self {
26        self.policy_kind = ReadPolicyKind::Detail;
27        self
28    }
29
30    pub fn as_list(mut self) -> Self {
31        self.policy_kind = ReadPolicyKind::List;
32        self
33    }
34
35    pub fn for_update(mut self) -> Self {
36        self.for_update = true;
37        self
38    }
39
40    pub async fn run(
41        self,
42        ctx: &CratestackContext,
43    ) -> Result<Option<cratestack_sql::Projection<M>>, CratestackError>
44    where
45        M: crate::FromPartialPgRow,
46        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
47    {
48        // Inside an `@isolation` procedure: run on its transaction
49        // (docs/design/procedure-isolation.md §4).
50        if let Some(bound) = self.runtime.bound() {
51            return crate::bound::in_bound_savepoint!(bound, |sp| self.run_in_tx(sp, ctx));
52        }
53        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
54        query
55            .push(self.descriptor.select_projection_subset(&self.selected))
56            .push(" FROM ")
57            .push(self.descriptor.table_name());
58        push_scoped_conditions(
59            &mut query,
60            self.descriptor,
61            &[],
62            Some((self.descriptor.primary_key(), self.id)),
63            ctx,
64            self.policy_kind,
65        );
66        query.push(" LIMIT 1");
67        if self.for_update {
68            query.push(" FOR UPDATE");
69        }
70
71        let row = query
72            .build()
73            .fetch_optional(self.runtime.pool())
74            .await
75            .map_err(crate::error::cratestack_error_from_sqlx)?;
76        decode_optional(row, &self.selected)
77    }
78
79    pub async fn run_in_tx<'tx>(
80        self,
81        tx: &mut sqlx::Transaction<'tx, sqlx::Postgres>,
82        ctx: &CratestackContext,
83    ) -> Result<Option<cratestack_sql::Projection<M>>, CratestackError>
84    where
85        M: crate::FromPartialPgRow,
86        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
87    {
88        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
89        query
90            .push(self.descriptor.select_projection_subset(&self.selected))
91            .push(" FROM ")
92            .push(self.descriptor.table_name());
93        push_scoped_conditions(
94            &mut query,
95            self.descriptor,
96            &[],
97            Some((self.descriptor.primary_key(), self.id)),
98            ctx,
99            self.policy_kind,
100        );
101        query.push(" LIMIT 1");
102        if self.for_update {
103            query.push(" FOR UPDATE");
104        }
105
106        let row = query
107            .build()
108            .fetch_optional(&mut **tx)
109            .await
110            .map_err(crate::error::cratestack_error_from_sqlx)?;
111        decode_optional(row, &self.selected)
112    }
113}
114
115fn decode_optional<M>(
116    row: Option<sqlx::postgres::PgRow>,
117    selected: &[&'static str],
118) -> Result<Option<cratestack_sql::Projection<M>>, CratestackError>
119where
120    M: crate::FromPartialPgRow,
121{
122    match row {
123        Some(row) => {
124            let value = M::decode_partial_pg_row(&row, selected)
125                .map_err(crate::error::cratestack_error_from_sqlx)?;
126            Ok(Some(cratestack_sql::Projection {
127                value,
128                selected: selected.to_vec(),
129            }))
130        }
131        None => Ok(None),
132    }
133}
134
135impl<'a, M: 'static, PK: 'static> FindUnique<'a, M, PK> {
136    /// Restrict the SELECT to the named columns. Resolves to
137    /// `Option<Projection<M>>` rather than `Option<M>`; non-selected
138    /// fields on the inner `M` hold `Default::default()`.
139    pub fn select<I, C>(self, columns: I) -> ProjectedFindUnique<'a, M, PK>
140    where
141        I: IntoIterator<Item = C>,
142        C: IntoColumnName,
143    {
144        ProjectedFindUnique {
145            runtime: self.runtime,
146            descriptor: self.descriptor,
147            id: self.id,
148            selected: columns
149                .into_iter()
150                .map(IntoColumnName::into_column_name)
151                .collect(),
152            policy_kind: self.policy_kind,
153            for_update: self.for_update,
154        }
155    }
156}