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