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        query::{
10            explain::ExplainPlan,
11            expr::{FilterExpr, OrderTerm},
12            intent::{Query, QueryError},
13            trace::QueryTracePlan,
14        },
15        response::ResponseError,
16    },
17    entity::{EntityKind, EntityValue, SingletonEntity},
18    types::Id,
19    value::OutputValue,
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    /// Project typed deleted rows through the accepted catalog pinned to this
44    /// session.
45    #[doc(hidden)]
46    pub fn project_entity_output_values(
47        &self,
48        entity: &E,
49        slots: &[usize],
50    ) -> Result<Vec<OutputValue>, crate::error::InternalError>
51    where
52        E: EntityValue,
53    {
54        self.session.project_entity_output_values(entity, slots)
55    }
56
57    pub(in crate::db) const fn new(session: &'a DbSession<E::Canister>, query: Query<E>) -> Self {
58        Self { session, query }
59    }
60
61    // ------------------------------------------------------------------
62    // Intent inspection
63    // ------------------------------------------------------------------
64
65    /// Borrow the current immutable query intent.
66    #[must_use]
67    pub(in crate::db) const fn query(&self) -> &Query<E> {
68        &self.query
69    }
70
71    fn map_query(mut self, map: impl FnOnce(Query<E>) -> Query<E>) -> Self {
72        self.query = map(self.query);
73        self
74    }
75
76    // Run one read-only session/query projection without mutating the delete
77    // builder shell so diagnostics and planning surfaces share one handoff
78    // shape from the fluent delete boundary into the session/query layer.
79    fn map_session_query_output<T>(
80        &self,
81        map: impl FnOnce(&DbSession<E::Canister>, &Query<E>) -> Result<T, QueryError>,
82    ) -> Result<T, QueryError> {
83        map(self.session, self.query())
84    }
85
86    // ------------------------------------------------------------------
87    // Intent builders (pure)
88    // ------------------------------------------------------------------
89
90    /// Set the access path to a single typed primary-key value.
91    ///
92    /// `Id<E>` is treated as a plain query input value here. It does not grant access.
93    #[must_use]
94    pub fn by_id(self, id: Id<E>) -> Self {
95        self.map_query(|query| query.by_id(id.key()))
96    }
97
98    /// Set the access path to multiple typed primary-key values.
99    ///
100    /// IDs are public and may come from untrusted input sources.
101    #[must_use]
102    pub fn by_ids<I>(self, ids: I) -> Self
103    where
104        I: IntoIterator<Item = Id<E>>,
105    {
106        self.map_query(|query| query.by_ids(ids.into_iter().map(|id| id.key())))
107    }
108
109    // ------------------------------------------------------------------
110    // Query Refinement
111    // ------------------------------------------------------------------
112
113    /// Add one typed filter expression directly.
114    #[must_use]
115    pub fn filter(self, expr: impl Into<FilterExpr>) -> Self {
116        self.map_query(|query| query.filter(expr))
117    }
118
119    /// Append one typed ORDER BY term.
120    #[must_use]
121    pub fn order_term(self, term: OrderTerm) -> Self {
122        self.map_query(|query| query.order_term(term))
123    }
124
125    /// Append multiple typed ORDER BY terms in declaration order.
126    #[must_use]
127    pub fn order_terms<I>(self, terms: I) -> Self
128    where
129        I: IntoIterator<Item = OrderTerm>,
130    {
131        self.map_query(|query| query.order_terms(terms))
132    }
133
134    /// Set the maximum number of rows this delete may affect.
135    #[must_use]
136    pub fn max_affected(self, limit: u32) -> Self {
137        self.map_query(|query| query.limit(limit))
138    }
139
140    // ------------------------------------------------------------------
141    // Planning / diagnostics
142    // ------------------------------------------------------------------
143
144    /// Build explain metadata for the current query.
145    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
146        self.map_session_query_output(DbSession::explain_query_with_visible_indexes)
147    }
148
149    /// Return the stable plan hash for this query.
150    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
151        self.map_session_query_output(DbSession::query_plan_hash_hex_with_visible_indexes)
152    }
153
154    /// Build one trace payload without executing the query.
155    pub fn trace(&self) -> Result<QueryTracePlan, QueryError> {
156        self.map_session_query_output(DbSession::trace_query)
157    }
158
159    // ------------------------------------------------------------------
160    // Execution (minimal core surface)
161    // ------------------------------------------------------------------
162
163    /// Execute this delete and return the affected-row count.
164    pub fn execute(&self) -> Result<u32, QueryError>
165    where
166        E: EntityValue,
167    {
168        self.session.execute_delete_count(self.query())
169    }
170
171    /// Execute this delete and materialize deleted rows for one explicit
172    /// row-returning surface.
173    pub fn execute_rows(&self) -> Result<EntityResponse<E>, QueryError>
174    where
175        E: EntityValue,
176    {
177        self.session.execute_delete_rows(self.query())
178    }
179
180    /// Execute and return whether any rows were affected.
181    pub fn is_empty(&self) -> Result<bool, QueryError>
182    where
183        E: EntityValue,
184    {
185        Ok(self.execute()? == 0)
186    }
187
188    /// Execute and return the number of affected rows.
189    pub fn count(&self) -> Result<u32, QueryError>
190    where
191        E: EntityValue,
192    {
193        self.execute()
194    }
195
196    /// Execute and require exactly one affected row.
197    pub fn require_one(&self) -> Result<(), QueryError>
198    where
199        E: EntityValue,
200    {
201        match self.execute()? {
202            1 => Ok(()),
203            0 => Err(ResponseError::not_found(E::PATH).into()),
204            count => Err(ResponseError::not_unique(E::PATH, count).into()),
205        }
206    }
207
208    /// Execute and require at least one affected row.
209    pub fn require_some(&self) -> Result<(), QueryError>
210    where
211        E: EntityValue,
212    {
213        if self.execute()? == 0 {
214            return Err(ResponseError::not_found(E::PATH).into());
215        }
216
217        Ok(())
218    }
219}
220
221impl<E> FluentDeleteQuery<'_, E>
222where
223    E: PersistedRow + SingletonEntity,
224    E::Key: Default,
225{
226    /// Delete the singleton entity.
227    #[must_use]
228    pub fn singleton(self) -> Self {
229        self.map_query(Query::singleton)
230    }
231}