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