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::{CoolContext, CoolError};
7use cratestack_sql::IntoColumnName;
8
9use crate::query::support::{ReadPolicyKind, push_scoped_conditions};
10use crate::{ModelDescriptor, SqlxRuntime, sqlx};
11
12use super::find_unique::FindUnique;
13
14#[derive(Debug, Clone)]
15pub struct ProjectedFindUnique<'a, M: 'static, PK: 'static> {
16    runtime: &'a SqlxRuntime,
17    descriptor: &'static ModelDescriptor<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: &CoolContext,
43    ) -> Result<Option<cratestack_sql::Projection<M>>, CoolError>
44    where
45        M: crate::FromPartialPgRow,
46        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
47    {
48        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
49        query
50            .push(self.descriptor.select_projection_subset(&self.selected))
51            .push(" FROM ")
52            .push(self.descriptor.table_name);
53        push_scoped_conditions(
54            &mut query,
55            self.descriptor,
56            &[],
57            Some((self.descriptor.primary_key, self.id)),
58            ctx,
59            self.policy_kind,
60        );
61        query.push(" LIMIT 1");
62        if self.for_update {
63            query.push(" FOR UPDATE");
64        }
65
66        let row = query
67            .build()
68            .fetch_optional(self.runtime.pool())
69            .await
70            .map_err(|error| CoolError::Database(error.to_string()))?;
71        decode_optional(row, &self.selected)
72    }
73
74    pub async fn run_in_tx<'tx>(
75        self,
76        tx: &mut sqlx::Transaction<'tx, sqlx::Postgres>,
77        ctx: &CoolContext,
78    ) -> Result<Option<cratestack_sql::Projection<M>>, CoolError>
79    where
80        M: crate::FromPartialPgRow,
81        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
82    {
83        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
84        query
85            .push(self.descriptor.select_projection_subset(&self.selected))
86            .push(" FROM ")
87            .push(self.descriptor.table_name);
88        push_scoped_conditions(
89            &mut query,
90            self.descriptor,
91            &[],
92            Some((self.descriptor.primary_key, self.id)),
93            ctx,
94            self.policy_kind,
95        );
96        query.push(" LIMIT 1");
97        if self.for_update {
98            query.push(" FOR UPDATE");
99        }
100
101        let row = query
102            .build()
103            .fetch_optional(&mut **tx)
104            .await
105            .map_err(|error| CoolError::Database(error.to_string()))?;
106        decode_optional(row, &self.selected)
107    }
108}
109
110fn decode_optional<M>(
111    row: Option<sqlx::postgres::PgRow>,
112    selected: &[&'static str],
113) -> Result<Option<cratestack_sql::Projection<M>>, CoolError>
114where
115    M: crate::FromPartialPgRow,
116{
117    match row {
118        Some(row) => {
119            let value = M::decode_partial_pg_row(&row, selected)
120                .map_err(|error| CoolError::Database(error.to_string()))?;
121            Ok(Some(cratestack_sql::Projection {
122                value,
123                selected: selected.to_vec(),
124            }))
125        }
126        None => Ok(None),
127    }
128}
129
130impl<'a, M: 'static, PK: 'static> FindUnique<'a, M, PK> {
131    /// Restrict the SELECT to the named columns. Resolves to
132    /// `Option<Projection<M>>` rather than `Option<M>`; non-selected
133    /// fields on the inner `M` hold `Default::default()`.
134    pub fn select<I, C>(self, columns: I) -> ProjectedFindUnique<'a, M, PK>
135    where
136        I: IntoIterator<Item = C>,
137        C: IntoColumnName,
138    {
139        ProjectedFindUnique {
140            runtime: self.runtime,
141            descriptor: self.descriptor,
142            id: self.id,
143            selected: columns
144                .into_iter()
145                .map(IntoColumnName::into_column_name)
146                .collect(),
147            policy_kind: self.policy_kind,
148            for_update: self.for_update,
149        }
150    }
151}