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, OrderTerm},
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    // ------------------------------------------------------------------
81    // Intent builders (pure)
82    // ------------------------------------------------------------------
83
84    /// Set the access path to a single typed primary-key value.
85    ///
86    /// `Id<E>` is treated as a plain query input value here. It does not grant access.
87    #[must_use]
88    pub fn by_id(self, id: Id<E>) -> Self {
89        self.map_query(|query| query.by_id(id.key()))
90    }
91
92    /// Set the access path to multiple typed primary-key values.
93    ///
94    /// IDs are public and may come from untrusted input sources.
95    #[must_use]
96    pub fn by_ids<I>(self, ids: I) -> Self
97    where
98        I: IntoIterator<Item = Id<E>>,
99    {
100        self.map_query(|query| query.by_ids(ids.into_iter().map(|id| id.key())))
101    }
102
103    // ------------------------------------------------------------------
104    // Query Refinement
105    // ------------------------------------------------------------------
106
107    /// Add a typed predicate expression directly.
108    #[must_use]
109    pub fn filter(self, predicate: Predicate) -> Self {
110        self.map_query(|query| query.filter(predicate))
111    }
112
113    /// Add a serialized filter expression after lowering and validation.
114    pub fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
115        self.try_map_query(|query| query.filter_expr(expr))
116    }
117
118    /// Append one typed ORDER BY term.
119    #[must_use]
120    pub fn order_term(self, term: OrderTerm) -> Self {
121        self.map_query(|query| query.order_term(term))
122    }
123
124    /// Append multiple typed ORDER BY terms in declaration order.
125    #[must_use]
126    pub fn order_terms<I>(self, terms: I) -> Self
127    where
128        I: IntoIterator<Item = OrderTerm>,
129    {
130        self.map_query(|query| query.order_terms(terms))
131    }
132
133    /// Bound the number of rows affected by this delete.
134    #[must_use]
135    pub fn limit(self, limit: u32) -> Self {
136        self.map_query(|query| query.limit(limit))
137    }
138
139    // ------------------------------------------------------------------
140    // Planning / diagnostics
141    // ------------------------------------------------------------------
142
143    /// Build explain metadata for the current query.
144    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
145        self.map_session_query_output(DbSession::explain_query_with_visible_indexes)
146    }
147
148    /// Return the stable plan hash for this query.
149    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
150        self.map_session_query_output(DbSession::query_plan_hash_hex_with_visible_indexes)
151    }
152
153    /// Build one trace payload without executing the query.
154    pub fn trace(&self) -> Result<QueryTracePlan, QueryError> {
155        self.map_session_query_output(DbSession::trace_query)
156    }
157
158    /// Build the validated logical plan without compiling execution details.
159    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
160        self.map_session_query_output(DbSession::planned_query_with_visible_indexes)
161    }
162
163    /// Build the compiled executable plan for this query.
164    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
165        self.map_session_query_output(DbSession::compile_query_with_visible_indexes)
166    }
167
168    // ------------------------------------------------------------------
169    // Execution (minimal core surface)
170    // ------------------------------------------------------------------
171
172    /// Execute this delete and return the affected-row count.
173    pub fn execute(&self) -> Result<u32, QueryError>
174    where
175        E: EntityValue,
176    {
177        self.session.execute_delete_count(self.query())
178    }
179
180    /// Execute this delete and materialize deleted rows for one explicit
181    /// row-returning surface.
182    pub fn execute_rows(&self) -> Result<EntityResponse<E>, QueryError>
183    where
184        E: EntityValue,
185    {
186        self.session.execute_query(self.query())
187    }
188
189    /// Execute and return whether any rows were affected.
190    pub fn is_empty(&self) -> Result<bool, QueryError>
191    where
192        E: EntityValue,
193    {
194        Ok(self.execute()? == 0)
195    }
196
197    /// Execute and return the number of affected rows.
198    pub fn count(&self) -> Result<u32, QueryError>
199    where
200        E: EntityValue,
201    {
202        self.execute()
203    }
204
205    /// Execute and require exactly one affected row.
206    pub fn require_one(&self) -> Result<(), QueryError>
207    where
208        E: EntityValue,
209    {
210        match self.execute()? {
211            1 => Ok(()),
212            0 => Err(ResponseError::not_found(E::PATH).into()),
213            count => Err(ResponseError::not_unique(E::PATH, count).into()),
214        }
215    }
216
217    /// Execute and require at least one affected row.
218    pub fn require_some(&self) -> Result<(), QueryError>
219    where
220        E: EntityValue,
221    {
222        if self.execute()? == 0 {
223            return Err(ResponseError::not_found(E::PATH).into());
224        }
225
226        Ok(())
227    }
228}
229
230impl<E> FluentDeleteQuery<'_, E>
231where
232    E: PersistedRow + SingletonEntity,
233    E::Key: Default,
234{
235    /// Delete the singleton entity.
236    #[must_use]
237    pub fn only(self) -> Self {
238        self.map_query(Query::only)
239    }
240}