Skip to main content

icydb_core/db/query/fluent/
delete.rs

1//! Module: query::fluent::delete
2//! Responsibility: fluent delete-query builder and execution routing.
3//! Does not own: query semantic validation or response projection.
4//! Boundary: session API facade over query intent/planning/execution.
5
6use crate::{
7    db::{
8        DbSession, EntityResponse, PersistedRow,
9        predicate::Predicate,
10        query::{
11            explain::ExplainPlan,
12            expr::{FilterExpr, SortExpr},
13            intent::{CompiledQuery, PlannedQuery, Query, QueryError},
14            trace::QueryTracePlan,
15        },
16        response::ResponseError,
17    },
18    traits::{EntityKind, EntityValue, SingletonEntity},
19    types::Id,
20};
21
22///
23/// FluentDeleteQuery
24///
25/// Session-bound delete query wrapper.
26/// This type owns *intent construction* and *execution routing only*.
27/// Delete execution follows the same traditional mutation contract as the
28/// unified SQL write lane: bare execution returns affected-row count.
29///
30
31pub struct FluentDeleteQuery<'a, E>
32where
33    E: EntityKind,
34{
35    session: &'a DbSession<E::Canister>,
36    query: Query<E>,
37}
38
39impl<'a, E> FluentDeleteQuery<'a, E>
40where
41    E: PersistedRow,
42{
43    pub(crate) const fn new(session: &'a DbSession<E::Canister>, query: Query<E>) -> Self {
44        Self { session, query }
45    }
46
47    // ------------------------------------------------------------------
48    // Intent inspection
49    // ------------------------------------------------------------------
50
51    /// Borrow the current immutable query intent.
52    #[must_use]
53    pub const fn query(&self) -> &Query<E> {
54        &self.query
55    }
56
57    fn map_query(mut self, map: impl FnOnce(Query<E>) -> Query<E>) -> Self {
58        self.query = map(self.query);
59        self
60    }
61
62    fn try_map_query(
63        mut self,
64        map: impl FnOnce(Query<E>) -> Result<Query<E>, QueryError>,
65    ) -> Result<Self, QueryError> {
66        self.query = map(self.query)?;
67        Ok(self)
68    }
69
70    // Run one read-only session/query projection without mutating the delete
71    // builder shell so diagnostics and planning surfaces share one handoff
72    // shape from the fluent delete boundary into the session/query layer.
73    fn map_session_query_output<T>(
74        &self,
75        map: impl FnOnce(&DbSession<E::Canister>, &Query<E>) -> Result<T, QueryError>,
76    ) -> Result<T, QueryError> {
77        map(self.session, self.query())
78    }
79
80    // Shared delete-count execution keeps the mutation contract in one place
81    // for public row-count terminals that differ only in response shaping.
82    fn execute_delete_count(&self) -> Result<u32, QueryError>
83    where
84        E: EntityValue,
85    {
86        self.session.execute_delete_count(self.query())
87    }
88
89    // Shared row-count classification keeps the delete facade aligned on the
90    // same not-found vs not-unique contract for exact-row requirements.
91    fn require_delete_row_count(&self, expected: u32) -> Result<(), QueryError>
92    where
93        E: EntityValue,
94    {
95        let row_count = self.execute_delete_count()?;
96
97        match row_count {
98            count if count == expected => Ok(()),
99            0 => Err(ResponseError::not_found(E::PATH).into()),
100            count => Err(ResponseError::not_unique(E::PATH, count).into()),
101        }
102    }
103
104    // ------------------------------------------------------------------
105    // Intent builders (pure)
106    // ------------------------------------------------------------------
107
108    /// Set the access path to a single typed primary-key value.
109    ///
110    /// `Id<E>` is treated as a plain query input value here. It does not grant access.
111    #[must_use]
112    pub fn by_id(self, id: Id<E>) -> Self {
113        self.map_query(|query| query.by_id(id.key()))
114    }
115
116    /// Set the access path to multiple typed primary-key values.
117    ///
118    /// IDs are public and may come from untrusted input sources.
119    #[must_use]
120    pub fn by_ids<I>(self, ids: I) -> Self
121    where
122        I: IntoIterator<Item = Id<E>>,
123    {
124        self.map_query(|query| query.by_ids(ids.into_iter().map(|id| id.key())))
125    }
126
127    // ------------------------------------------------------------------
128    // Query Refinement
129    // ------------------------------------------------------------------
130
131    /// Add a typed predicate expression directly.
132    #[must_use]
133    pub fn filter(self, predicate: Predicate) -> Self {
134        self.map_query(|query| query.filter(predicate))
135    }
136
137    /// Add a serialized filter expression after lowering and validation.
138    pub fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
139        self.try_map_query(|query| query.filter_expr(expr))
140    }
141
142    /// Add sort clauses from a serialized sort expression.
143    pub fn sort_expr(self, expr: SortExpr) -> Result<Self, QueryError> {
144        self.try_map_query(|query| query.sort_expr(expr))
145    }
146
147    /// Append ascending order for one field.
148    #[must_use]
149    pub fn order_by(self, field: impl AsRef<str>) -> Self {
150        self.map_query(|query| query.order_by(field))
151    }
152
153    /// Append descending order for one field.
154    #[must_use]
155    pub fn order_by_desc(self, field: impl AsRef<str>) -> Self {
156        self.map_query(|query| query.order_by_desc(field))
157    }
158
159    /// Bound the number of rows affected by this delete.
160    #[must_use]
161    pub fn limit(self, limit: u32) -> Self {
162        self.map_query(|query| query.limit(limit))
163    }
164
165    // ------------------------------------------------------------------
166    // Planning / diagnostics
167    // ------------------------------------------------------------------
168
169    /// Build explain metadata for the current query.
170    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
171        self.map_session_query_output(DbSession::explain_query_with_visible_indexes)
172    }
173
174    /// Return the stable plan hash for this query.
175    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
176        self.map_session_query_output(DbSession::query_plan_hash_hex_with_visible_indexes)
177    }
178
179    /// Build one trace payload without executing the query.
180    pub fn trace(&self) -> Result<QueryTracePlan, QueryError> {
181        self.map_session_query_output(DbSession::trace_query)
182    }
183
184    /// Build the validated logical plan without compiling execution details.
185    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
186        self.map_session_query_output(DbSession::planned_query_with_visible_indexes)
187    }
188
189    /// Build the compiled executable plan for this query.
190    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
191        self.map_session_query_output(DbSession::compile_query_with_visible_indexes)
192    }
193
194    // ------------------------------------------------------------------
195    // Execution (minimal core surface)
196    // ------------------------------------------------------------------
197
198    /// Execute this delete and return the affected-row count.
199    pub fn execute(&self) -> Result<u32, QueryError>
200    where
201        E: EntityValue,
202    {
203        self.execute_delete_count()
204    }
205
206    /// Execute this delete and materialize deleted rows for one explicit
207    /// row-returning surface.
208    pub fn execute_rows(&self) -> Result<EntityResponse<E>, QueryError>
209    where
210        E: EntityValue,
211    {
212        self.session.execute_query(self.query())
213    }
214
215    /// Execute and return whether any rows were affected.
216    pub fn is_empty(&self) -> Result<bool, QueryError>
217    where
218        E: EntityValue,
219    {
220        Ok(self.execute()? == 0)
221    }
222
223    /// Execute and return the number of affected rows.
224    pub fn count(&self) -> Result<u32, QueryError>
225    where
226        E: EntityValue,
227    {
228        self.execute_delete_count()
229    }
230
231    /// Execute and require exactly one affected row.
232    pub fn require_one(&self) -> Result<(), QueryError>
233    where
234        E: EntityValue,
235    {
236        self.require_delete_row_count(1)
237    }
238
239    /// Execute and require at least one affected row.
240    pub fn require_some(&self) -> Result<(), QueryError>
241    where
242        E: EntityValue,
243    {
244        if self.execute()? == 0 {
245            return Err(ResponseError::not_found(E::PATH).into());
246        }
247
248        Ok(())
249    }
250}
251
252impl<E> FluentDeleteQuery<'_, E>
253where
254    E: PersistedRow + SingletonEntity,
255    E::Key: Default,
256{
257    /// Delete the singleton entity.
258    #[must_use]
259    pub fn only(self) -> Self {
260        self.map_query(Query::only)
261    }
262}