Skip to main content

sift_query_sqlx/execution/
external.rs

1//! Advanced entry points for a connection/transaction owned by the caller.
2//!
3//! These operations borrow the connection and leave all transaction and
4//! session configuration to its owner. On timeout or cancellation, the
5//! owner must recover or discard the connection before reusing it; the crate
6//! cannot close a borrowed connection. In a transaction, database errors can
7//! leave the transaction aborted and the caller must roll it back.
8
9use sift_query_core::{
10    GroupScope, QueryInput, QueryTarget, SearchEntity, SearchOptions, ServerScope,
11};
12use sqlx::FromRow;
13use sqlx::postgres::{PgConnection, PgRow};
14
15use crate::cache::RenderRequest;
16use crate::execution::io::{
17    fetch_cluster_external, fetch_maps_external, fetch_typed_external, prepare,
18};
19use crate::{ExecutionStrategy, Page, PgSearchError, PgSiftQuery, RowMap, TimeoutStage};
20
21impl PgSiftQuery {
22    /// Executes a typed page in an existing connection or transaction.
23    ///
24    /// Pass `&mut *transaction` for a sqlx transaction. This method does not
25    /// begin/commit/rollback transactions or modify `statement_timeout`.
26    pub async fn search_on<E>(
27        &self,
28        connection: &mut PgConnection,
29        query: impl QueryInput<E>,
30        scope: &ServerScope<E>,
31    ) -> Result<Page<E>, PgSearchError>
32    where
33        E: SearchEntity + Send + Unpin + for<'row> FromRow<'row, PgRow>,
34    {
35        self.search_on_with(connection, query, scope, &GroupScope::unrestricted(), None)
36            .await
37    }
38
39    /// Typed external execution with server-owned HAVING and summaries.
40    pub async fn search_on_with<E>(
41        &self,
42        connection: &mut PgConnection,
43        query: impl QueryInput<E>,
44        scope: &ServerScope<E>,
45        group_scope: &GroupScope<E>,
46        options: Option<&SearchOptions<E>>,
47    ) -> Result<Page<E>, PgSearchError>
48    where
49        E: SearchEntity + Send + Unpin + for<'row> FromRow<'row, PgRow>,
50    {
51        self.observe::<E, _>(
52            "search_on",
53            async {
54                self.validate_external_strategy()?;
55                let plan = self.resolve_plan_with_scopes(
56                    query,
57                    scope,
58                    group_scope,
59                    QueryTarget::TypedPage,
60                    "search_on",
61                )?;
62                if plan.force_empty {
63                    return Ok(empty_page());
64                }
65                let partition = self.extensions.resolve_partition(&plan, "search_on")?;
66                let list = (plan.paging.size > 0)
67                    .then(|| {
68                        self.render(
69                            &plan,
70                            "search_on",
71                            "list",
72                            partition.as_deref(),
73                            RenderRequest::List,
74                        )
75                    })
76                    .transpose()?
77                    .map(prepare)
78                    .transpose()?;
79                let cluster = prepare(self.render(
80                    &plan,
81                    "search_on",
82                    "cluster",
83                    partition.as_deref(),
84                    RenderRequest::Cluster(options),
85                )?)?;
86                let deadline = self.deadline::<E>();
87                let data = match list {
88                    Some(list) => {
89                        fetch_typed_external(connection, list, deadline, TimeoutStage::ListQuery)
90                            .await?
91                    }
92                    None => Vec::new(),
93                };
94                let (total_count, summaries) =
95                    fetch_cluster_external(connection, cluster, options, E::META, deadline).await?;
96                let next_cursor = if data.len() == plan.paging.size as usize {
97                    self.entity_cursor(&plan, data.last())?
98                } else {
99                    None
100                };
101                Ok(Page {
102                    data,
103                    total_count,
104                    summaries,
105                    next_cursor,
106                })
107            },
108            |page| page.data.len(),
109        )
110        .await
111    }
112
113    /// Executes a dynamic map page on a caller-owned connection.
114    pub async fn search_map_on<E>(
115        &self,
116        connection: &mut PgConnection,
117        query: impl QueryInput<E>,
118        scope: &ServerScope<E>,
119    ) -> Result<Page<RowMap>, PgSearchError>
120    where
121        E: SearchEntity,
122    {
123        self.search_map_on_with(connection, query, scope, &GroupScope::unrestricted(), None)
124            .await
125    }
126
127    /// Dynamic external execution with server-owned HAVING and summaries.
128    pub async fn search_map_on_with<E>(
129        &self,
130        connection: &mut PgConnection,
131        query: impl QueryInput<E>,
132        scope: &ServerScope<E>,
133        group_scope: &GroupScope<E>,
134        options: Option<&SearchOptions<E>>,
135    ) -> Result<Page<RowMap>, PgSearchError>
136    where
137        E: SearchEntity,
138    {
139        self.observe::<E, _>(
140            "search_map_on",
141            async {
142                self.validate_external_strategy()?;
143                let plan = self.resolve_plan_with_scopes(
144                    query,
145                    scope,
146                    group_scope,
147                    QueryTarget::MapPage,
148                    "search_map_on",
149                )?;
150                if plan.force_empty {
151                    return Ok(empty_page());
152                }
153                let partition = self.extensions.resolve_partition(&plan, "search_map_on")?;
154                let list = (plan.paging.size > 0)
155                    .then(|| {
156                        self.render(
157                            &plan,
158                            "search_map_on",
159                            "list",
160                            partition.as_deref(),
161                            RenderRequest::List,
162                        )
163                    })
164                    .transpose()?
165                    .map(prepare)
166                    .transpose()?;
167                let cluster = prepare(self.render(
168                    &plan,
169                    "search_map_on",
170                    "cluster",
171                    partition.as_deref(),
172                    RenderRequest::Cluster(options),
173                )?)?;
174                let deadline = self.deadline::<E>();
175                let mut data = match list {
176                    Some(list) => fetch_maps_external(connection, list, &plan, deadline).await?,
177                    None => Vec::new(),
178                };
179                let (total_count, summaries) =
180                    fetch_cluster_external(connection, cluster, options, E::META, deadline).await?;
181                let next_cursor = if data.len() == plan.paging.size as usize {
182                    self.map_cursor(&plan, data.last())?
183                } else {
184                    None
185                };
186                self.format_map_rows::<E>(&mut data)?;
187                self.extensions
188                    .filter_rows::<E>("search_map_on", &mut data)?;
189                Ok(self
190                    .extensions
191                    .enrich_page(
192                        E::META.entity_name,
193                        "search_map_on",
194                        Page {
195                            data,
196                            total_count,
197                            summaries,
198                            next_cursor,
199                        },
200                    )
201                    .await?)
202            },
203            |page| page.data.len(),
204        )
205        .await
206    }
207
208    pub(crate) fn validate_external_strategy(&self) -> Result<(), PgSearchError> {
209        if self.execution.strategy != ExecutionStrategy::SequentialBestEffort {
210            return Err(PgSearchError::ExternalExecutionStrategy);
211        }
212        Ok(())
213    }
214}
215
216fn empty_page<T>() -> Page<T> {
217    Page::empty()
218}