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