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