Skip to main content

icydb/db/session/
mod.rs

1pub mod delete;
2pub(crate) mod generated;
3pub mod load;
4mod macros;
5
6#[cfg(feature = "sql")]
7use crate::db::sql::{SqlProjectionRows, SqlQueryResult, SqlQueryRowsOutput, render_value_text};
8use crate::{
9    db::{
10        EntityFieldDescription, EntitySchemaDescription, StorageReport,
11        query::{MissingRowPolicy, Query, QueryTracePlan},
12        response::QueryResponse,
13    },
14    error::{Error, ErrorKind, ErrorOrigin, RuntimeErrorKind},
15    metrics::MetricsSink,
16    traits::{CanisterKind, Entity},
17    value::{InputValue, OutputValue},
18};
19use icydb_core as core;
20
21// re-exports
22pub use delete::SessionDeleteQuery;
23pub use load::{FluentLoadQuery, PagedLoadQuery};
24
25///
26/// MutationMode
27///
28/// Public write-mode contract for structural session mutations.
29/// This keeps insert, update, and replace under one API surface instead of
30/// freezing separate partial helpers with divergent semantics.
31///
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum MutationMode {
35    Insert,
36    Replace,
37    Update,
38}
39
40impl MutationMode {
41    const fn into_core(self) -> core::db::MutationMode {
42        match self {
43            Self::Insert => core::db::MutationMode::Insert,
44            Self::Replace => core::db::MutationMode::Replace,
45            Self::Update => core::db::MutationMode::Update,
46        }
47    }
48}
49
50/// SQL query attribution envelope used by generated canister endpoints.
51#[cfg(feature = "sql")]
52#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
53pub struct SqlQueryPerfAttribution {
54    pub compile_local_instructions: u64,
55    pub execution: SqlExecutionPerfAttribution,
56    pub pure_covering: Option<SqlPureCoveringPerfAttribution>,
57    pub response_decode_local_instructions: u64,
58    pub total_local_instructions: u64,
59}
60
61/// SQL execution-stage attribution.
62#[cfg(feature = "sql")]
63#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
64pub struct SqlExecutionPerfAttribution {
65    pub planner_local_instructions: u64,
66    pub store_local_instructions: u64,
67    pub executor_local_instructions: u64,
68}
69
70/// SQL pure-covering attribution.
71#[cfg(feature = "sql")]
72#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
73pub struct SqlPureCoveringPerfAttribution {
74    pub decode_local_instructions: u64,
75    pub row_assembly_local_instructions: u64,
76}
77
78#[cfg(all(feature = "sql", feature = "diagnostics"))]
79impl From<crate::db::SqlQueryExecutionAttribution> for SqlQueryPerfAttribution {
80    fn from(attribution: crate::db::SqlQueryExecutionAttribution) -> Self {
81        Self {
82            compile_local_instructions: attribution.compile_local_instructions,
83            execution: SqlExecutionPerfAttribution {
84                planner_local_instructions: attribution.execution.planner_local_instructions,
85                store_local_instructions: attribution.execution.store_local_instructions,
86                executor_local_instructions: attribution.execution.executor_local_instructions,
87            },
88            pure_covering: attribution.pure_covering.map(|pure_covering| {
89                SqlPureCoveringPerfAttribution {
90                    decode_local_instructions: pure_covering.decode_local_instructions,
91                    row_assembly_local_instructions: pure_covering.row_assembly_local_instructions,
92                }
93            }),
94            response_decode_local_instructions: attribution.response_decode_local_instructions,
95            total_local_instructions: attribution.total_local_instructions,
96        }
97    }
98}
99
100///
101/// StructuralPatch
102///
103/// Public structural mutation patch wrapper.
104/// Public callers should construct field-bearing patches through
105/// `DbSession::structural_patch(...)` so field lookup follows the accepted
106/// persisted schema instead of generated model field order.
107/// Empty patches remain representable for callers that need to explicitly
108/// exercise sparse mutation behavior.
109///
110
111#[derive(Default)]
112pub struct StructuralPatch {
113    inner: core::db::StructuralPatch,
114}
115
116impl StructuralPatch {
117    /// Build one empty structural patch.
118    ///
119    /// Use `DbSession::structural_patch(...)` for patches with field updates.
120    #[must_use]
121    pub const fn new() -> Self {
122        Self {
123            inner: core::db::StructuralPatch::new(),
124        }
125    }
126
127    const fn from_core(inner: core::db::StructuralPatch) -> Self {
128        Self { inner }
129    }
130}
131
132///
133/// DbSession
134///
135/// Public facade for session-scoped query execution, typed SQL lowering, and
136/// structural mutation policy.
137/// Wraps the core session and converts core results and errors into the
138/// outward-facing `icydb` response surface.
139///
140
141pub struct DbSession<C: CanisterKind> {
142    inner: core::db::DbSession<C>,
143}
144
145#[cfg(all(feature = "sql", feature = "diagnostics"))]
146#[expect(clippy::missing_const_for_fn)]
147fn read_sql_response_decode_local_instruction_counter() -> u64 {
148    #[cfg(target_arch = "wasm32")]
149    {
150        canic_cdk::api::performance_counter(1)
151    }
152
153    #[cfg(not(target_arch = "wasm32"))]
154    {
155        0
156    }
157}
158
159#[cfg(all(feature = "sql", feature = "diagnostics"))]
160fn measure_sql_response_decode_stage<T>(run: impl FnOnce() -> T) -> (u64, T) {
161    let start = read_sql_response_decode_local_instruction_counter();
162    let result = run();
163    let delta = read_sql_response_decode_local_instruction_counter().saturating_sub(start);
164
165    (delta, result)
166}
167
168// Fold the public SQL response-packaging phase onto the outward top-level perf
169// contract so shell-facing totals remain exhaustive across compile, planner,
170// store, executor, and decode.
171#[cfg(all(feature = "sql", feature = "diagnostics"))]
172const fn finalize_public_sql_query_attribution(
173    mut attribution: crate::db::SqlQueryExecutionAttribution,
174    response_decode_local_instructions: u64,
175) -> crate::db::SqlQueryExecutionAttribution {
176    attribution.response_decode_local_instructions = response_decode_local_instructions;
177    attribution.execute_local_instructions = attribution
178        .execution
179        .planner_local_instructions
180        .saturating_add(attribution.execution.store_local_instructions)
181        .saturating_add(attribution.execution.executor_local_instructions)
182        .saturating_add(
183            attribution
184                .execution
185                .response_finalization_local_instructions,
186        )
187        .saturating_add(response_decode_local_instructions);
188    attribution.total_local_instructions = attribution
189        .compile_local_instructions
190        .saturating_add(attribution.execute_local_instructions);
191
192    attribution
193}
194
195impl<C: CanisterKind> DbSession<C> {
196    fn query_response_from_core<E>(inner: core::db::LoadQueryResult<E>) -> QueryResponse<E>
197    where
198        E: Entity,
199    {
200        QueryResponse::from_core(inner)
201    }
202
203    // ------------------------------------------------------------------
204    // Session configuration
205    // ------------------------------------------------------------------
206
207    #[must_use]
208    pub const fn new(session: core::db::DbSession<C>) -> Self {
209        Self { inner: session }
210    }
211
212    #[must_use]
213    pub const fn debug(mut self) -> Self {
214        self.inner = self.inner.debug();
215        self
216    }
217
218    #[must_use]
219    pub fn metrics_sink(mut self, sink: &'static dyn MetricsSink) -> Self {
220        self.inner = self.inner.metrics_sink(sink);
221        self
222    }
223
224    // ------------------------------------------------------------------
225    // Query entry points
226    // ------------------------------------------------------------------
227
228    #[must_use]
229    pub const fn load<E>(&self) -> FluentLoadQuery<'_, E>
230    where
231        E: crate::traits::EntityFor<C>,
232    {
233        FluentLoadQuery {
234            inner: self.inner.load::<E>(),
235        }
236    }
237
238    #[must_use]
239    pub const fn load_with_consistency<E>(
240        &self,
241        consistency: MissingRowPolicy,
242    ) -> FluentLoadQuery<'_, E>
243    where
244        E: crate::traits::EntityFor<C>,
245    {
246        FluentLoadQuery {
247            inner: self.inner.load_with_consistency::<E>(consistency),
248        }
249    }
250
251    /// Execute one typed/fluent query while reporting the compile/execute
252    /// split at the shared query seam.
253    #[cfg(feature = "diagnostics")]
254    #[doc(hidden)]
255    pub fn execute_query_result_with_attribution<E>(
256        &self,
257        query: &Query<E>,
258    ) -> Result<(QueryResponse<E>, crate::db::QueryExecutionAttribution), Error>
259    where
260        E: crate::traits::EntityFor<C>,
261    {
262        let (result, attribution) = self.inner.execute_query_result_with_attribution(query)?;
263
264        Ok((Self::query_response_from_core(result), attribution))
265    }
266
267    /// Execute one reduced SQL query against one concrete entity type.
268    #[cfg(feature = "sql")]
269    pub fn execute_sql_query<E>(&self, sql: &str) -> Result<SqlQueryResult, Error>
270    where
271        E: crate::traits::EntityFor<C>,
272    {
273        Ok(crate::db::sql::sql_query_result_from_statement(
274            self.inner.execute_sql_query::<E>(sql)?,
275            E::MODEL.name().to_string(),
276        ))
277    }
278
279    /// Execute one SQL query and return the shell perf envelope shape.
280    #[cfg(all(feature = "sql", not(feature = "diagnostics")))]
281    #[doc(hidden)]
282    pub fn execute_sql_query_with_perf_attribution<E>(
283        &self,
284        sql: &str,
285    ) -> Result<(SqlQueryResult, SqlQueryPerfAttribution), Error>
286    where
287        E: crate::traits::EntityFor<C>,
288    {
289        Ok((
290            self.execute_sql_query::<E>(sql)?,
291            SqlQueryPerfAttribution::default(),
292        ))
293    }
294
295    /// Execute one reduced SQL query and report the top-level compile/execute
296    /// cost split at the SQL seam.
297    #[cfg(all(feature = "sql", feature = "diagnostics"))]
298    #[doc(hidden)]
299    pub fn execute_sql_query_with_perf_attribution<E>(
300        &self,
301        sql: &str,
302    ) -> Result<(SqlQueryResult, SqlQueryPerfAttribution), Error>
303    where
304        E: crate::traits::EntityFor<C>,
305    {
306        let (result, mut attribution) = self.inner.execute_sql_query_with_attribution::<E>(sql)?;
307        let entity_name = E::MODEL.name().to_string();
308
309        // Phase 1: measure the outward SQL response packaging step separately
310        // so shell/dev perf output can distinguish executor work from result
311        // decode and formatting prep.
312        let (response_decode_local_instructions, result) =
313            measure_sql_response_decode_stage(|| {
314                crate::db::sql::sql_query_result_from_statement(result, entity_name)
315            });
316        attribution =
317            finalize_public_sql_query_attribution(attribution, response_decode_local_instructions);
318
319        Ok((result, SqlQueryPerfAttribution::from(attribution)))
320    }
321
322    /// Execute one reduced SQL query and report the top-level compile/execute
323    /// cost split at the SQL seam.
324    #[cfg(all(feature = "sql", feature = "diagnostics"))]
325    #[doc(hidden)]
326    pub fn execute_sql_query_with_attribution<E>(
327        &self,
328        sql: &str,
329    ) -> Result<(SqlQueryResult, crate::db::SqlQueryExecutionAttribution), Error>
330    where
331        E: crate::traits::EntityFor<C>,
332    {
333        let (result, mut attribution) = self.inner.execute_sql_query_with_attribution::<E>(sql)?;
334        let entity_name = E::MODEL.name().to_string();
335        let (response_decode_local_instructions, result) =
336            measure_sql_response_decode_stage(|| {
337                crate::db::sql::sql_query_result_from_statement(result, entity_name)
338            });
339        attribution =
340            finalize_public_sql_query_attribution(attribution, response_decode_local_instructions);
341
342        Ok((result, attribution))
343    }
344
345    /// Execute one reduced SQL mutation statement against one concrete entity type.
346    #[cfg(feature = "sql")]
347    pub fn execute_sql_update<E>(&self, sql: &str) -> Result<SqlQueryResult, Error>
348    where
349        E: crate::traits::EntityFor<C>,
350    {
351        Ok(crate::db::sql::sql_query_result_from_statement(
352            self.inner.execute_sql_update::<E>(sql)?,
353            E::MODEL.name().to_string(),
354        ))
355    }
356
357    /// Execute one supported SQL DDL statement against one concrete entity type.
358    #[cfg(feature = "sql")]
359    pub fn execute_sql_ddl<E>(&self, sql: &str) -> Result<SqlQueryResult, Error>
360    where
361        E: crate::traits::EntityFor<C>,
362    {
363        Ok(crate::db::sql::sql_query_result_from_statement(
364            self.inner.execute_sql_ddl::<E>(sql)?,
365            E::MODEL.name().to_string(),
366        ))
367    }
368
369    #[cfg(feature = "sql")]
370    fn projection_selection<E>(
371        selected_fields: Option<&[String]>,
372    ) -> Result<(Vec<String>, Vec<usize>), Error>
373    where
374        E: crate::traits::EntityFor<C>,
375    {
376        match selected_fields {
377            None => Ok((
378                E::MODEL
379                    .fields()
380                    .iter()
381                    .map(|field| field.name().to_string())
382                    .collect(),
383                (0..E::MODEL.fields().len()).collect(),
384            )),
385            Some(fields) => {
386                let mut indices = Vec::with_capacity(fields.len());
387
388                for field in fields {
389                    let index = E::MODEL
390                        .fields()
391                        .iter()
392                        .position(|candidate| candidate.name() == field.as_str())
393                        .ok_or_else(|| {
394                            Error::new(
395                                ErrorKind::Runtime(RuntimeErrorKind::Unsupported),
396                                ErrorOrigin::Query,
397                                format!(
398                                    "RETURNING field '{field}' does not exist on the target entity '{}'",
399                                    E::PATH
400                                ),
401                            )
402                        })?;
403                    indices.push(index);
404                }
405
406                Ok((fields.to_vec(), indices))
407            }
408        }
409    }
410
411    #[cfg(feature = "sql")]
412    pub(crate) fn sql_query_rows_output_from_entities<E>(
413        entity_name: String,
414        entities: Vec<E>,
415        selected_fields: Option<&[String]>,
416    ) -> Result<SqlQueryRowsOutput, Error>
417    where
418        E: crate::traits::EntityFor<C>,
419    {
420        // Phase 1: resolve the explicit outward projection contract before
421        // rendering any row data so every row-producing typed write helper
422        // shares one field-selection rule.
423        let (columns, indices) = Self::projection_selection::<E>(selected_fields)?;
424        let mut rows = Vec::with_capacity(entities.len());
425
426        // Phase 2: render the selected entity slots into stable SQL-style text
427        // rows so every row-producing write surface converges on the same
428        // outward payload family.
429        for entity in entities {
430            let mut rendered = Vec::with_capacity(indices.len());
431            for index in &indices {
432                let value = entity.get_value_by_index(*index).ok_or_else(|| {
433                    Error::new(
434                        ErrorKind::Runtime(RuntimeErrorKind::Internal),
435                        ErrorOrigin::Query,
436                        format!(
437                            "RETURNING projection row must align with declared columns: entity='{}' index={index}",
438                            E::PATH
439                        ),
440                    )
441                })?;
442                rendered.push(render_value_text(&OutputValue::from(value)));
443            }
444            rows.push(rendered);
445        }
446
447        let row_count = u32::try_from(rows.len()).unwrap_or(u32::MAX);
448
449        Ok(SqlQueryRowsOutput::from_projection(
450            entity_name,
451            SqlProjectionRows::new(columns, rows, row_count),
452        ))
453    }
454
455    #[cfg(feature = "sql")]
456    fn returning_fields<I, S>(fields: I) -> Vec<String>
457    where
458        I: IntoIterator<Item = S>,
459        S: AsRef<str>,
460    {
461        fields
462            .into_iter()
463            .map(|field| field.as_ref().to_string())
464            .collect()
465    }
466
467    #[cfg(feature = "sql")]
468    fn sql_query_rows_output_from_entity<E>(
469        entity: E,
470        selected_fields: Option<&[String]>,
471    ) -> Result<SqlQueryRowsOutput, Error>
472    where
473        E: crate::traits::EntityFor<C>,
474    {
475        Self::sql_query_rows_output_from_entities::<E>(
476            E::PATH.to_string(),
477            vec![entity],
478            selected_fields,
479        )
480    }
481
482    #[must_use]
483    pub fn delete<E>(&self) -> SessionDeleteQuery<'_, E>
484    where
485        E: crate::traits::EntityFor<C>,
486    {
487        SessionDeleteQuery {
488            inner: self.inner.delete::<E>(),
489        }
490    }
491
492    #[must_use]
493    pub fn delete_with_consistency<E>(
494        &self,
495        consistency: MissingRowPolicy,
496    ) -> SessionDeleteQuery<'_, E>
497    where
498        E: crate::traits::EntityFor<C>,
499    {
500        SessionDeleteQuery {
501            inner: self.inner.delete_with_consistency::<E>(consistency),
502        }
503    }
504
505    /// Return one stable, human-readable index listing for the entity schema.
506    #[must_use]
507    pub fn show_indexes<E>(&self) -> Vec<String>
508    where
509        E: crate::traits::EntityFor<C>,
510    {
511        self.inner.show_indexes::<E>()
512    }
513
514    /// Return one stable list of field descriptors for the entity schema.
515    #[must_use]
516    pub fn show_columns<E>(&self) -> Vec<EntityFieldDescription>
517    where
518        E: crate::traits::EntityFor<C>,
519    {
520        self.inner.show_columns::<E>()
521    }
522
523    /// Return one stable list of runtime-registered entity names.
524    #[must_use]
525    pub fn show_entities(&self) -> Vec<String> {
526        self.inner.show_entities()
527    }
528
529    /// Return one stable list of runtime-registered entity names.
530    ///
531    /// This is the typed alias of SQL `SHOW TABLES`, which itself aliases
532    /// `SHOW ENTITIES`.
533    #[must_use]
534    pub fn show_tables(&self) -> Vec<String> {
535        self.inner.show_tables()
536    }
537
538    /// Return one structured schema description for the entity.
539    #[must_use]
540    pub fn describe_entity<E>(&self) -> EntitySchemaDescription
541    where
542        E: crate::traits::EntityFor<C>,
543    {
544        self.inner.describe_entity::<E>()
545    }
546
547    /// Return one accepted live-schema description for the entity.
548    ///
549    /// Generated schema endpoints use this accepted-schema path so DDL-published
550    /// index metadata and recovered schema authority are reflected in tooling
551    /// payloads instead of only the compiled model proposal.
552    pub fn try_describe_entity<E>(&self) -> Result<EntitySchemaDescription, Error>
553    where
554        E: crate::traits::EntityFor<C>,
555    {
556        Ok(self.inner.try_describe_entity::<E>()?)
557    }
558
559    /// Build one point-in-time storage report for observability endpoints.
560    pub fn storage_report(
561        &self,
562        name_to_path: &[(&'static str, &'static str)],
563    ) -> Result<StorageReport, Error> {
564        Ok(self.inner.storage_report(name_to_path)?)
565    }
566
567    // ------------------------------------------------------------------
568    // Execution
569    // ------------------------------------------------------------------
570
571    pub fn execute_query<E>(&self, query: &Query<E>) -> Result<QueryResponse<E>, Error>
572    where
573        E: crate::traits::EntityFor<C>,
574    {
575        Ok(Self::query_response_from_core(
576            self.inner.execute_query_result(query)?,
577        ))
578    }
579
580    /// Build one trace payload for a query without executing it.
581    pub fn trace_query<E>(&self, query: &Query<E>) -> Result<QueryTracePlan, Error>
582    where
583        E: crate::traits::EntityFor<C>,
584    {
585        Ok(self.inner.trace_query(query)?)
586    }
587
588    // ------------------------------------------------------------------
589    // High-level write helpers (semantic)
590    // ------------------------------------------------------------------
591
592    pub fn insert<E>(&self, entity: E) -> Result<E, Error>
593    where
594        E: crate::traits::EntityFor<C>,
595    {
596        Ok(self.inner.insert(entity)?)
597    }
598
599    /// Insert one full entity and return every persisted field.
600    #[cfg(feature = "sql")]
601    pub fn insert_returning_all<E>(&self, entity: E) -> Result<SqlQueryRowsOutput, Error>
602    where
603        E: crate::traits::EntityFor<C>,
604    {
605        let entity = self.inner.insert(entity)?;
606
607        Self::sql_query_rows_output_from_entity::<E>(entity, None)
608    }
609
610    /// Insert one full entity and return one explicit field list.
611    #[cfg(feature = "sql")]
612    pub fn insert_returning<E, I, S>(
613        &self,
614        entity: E,
615        fields: I,
616    ) -> Result<SqlQueryRowsOutput, Error>
617    where
618        E: crate::traits::EntityFor<C>,
619        I: IntoIterator<Item = S>,
620        S: AsRef<str>,
621    {
622        let entity = self.inner.insert(entity)?;
623        let fields = Self::returning_fields(fields);
624
625        Self::sql_query_rows_output_from_entity::<E>(entity, Some(fields.as_slice()))
626    }
627
628    /// Create one authored typed input.
629    pub fn create<I>(&self, input: I) -> Result<I::Entity, Error>
630    where
631        I: crate::traits::CreateInputFor<C>,
632        I::Entity: crate::traits::EntityFor<C>,
633    {
634        Ok(self.inner.create(input)?)
635    }
636
637    /// Create one authored typed input and return every persisted field.
638    #[cfg(feature = "sql")]
639    pub fn create_returning_all<I>(&self, input: I) -> Result<SqlQueryRowsOutput, Error>
640    where
641        I: crate::traits::CreateInputFor<C>,
642        I::Entity: crate::traits::EntityFor<C>,
643    {
644        let entity = self.inner.create(input)?;
645
646        Self::sql_query_rows_output_from_entity::<I::Entity>(entity, None)
647    }
648
649    /// Create one authored typed input and return one explicit field list.
650    #[cfg(feature = "sql")]
651    pub fn create_returning<I, F, S>(
652        &self,
653        input: I,
654        fields: F,
655    ) -> Result<SqlQueryRowsOutput, Error>
656    where
657        I: crate::traits::CreateInputFor<C>,
658        I::Entity: crate::traits::EntityFor<C>,
659        F: IntoIterator<Item = S>,
660        S: AsRef<str>,
661    {
662        let entity = self.inner.create(input)?;
663        let fields = Self::returning_fields(fields);
664
665        Self::sql_query_rows_output_from_entity::<I::Entity>(entity, Some(fields.as_slice()))
666    }
667
668    /// Insert a single-entity-type batch atomically in one commit window.
669    ///
670    /// If any item fails pre-commit validation, no row in the batch is persisted.
671    ///
672    /// This API is not a multi-entity transaction surface.
673    pub fn insert_many_atomic<E>(
674        &self,
675        entities: impl IntoIterator<Item = E>,
676    ) -> Result<Vec<E>, Error>
677    where
678        E: crate::traits::EntityFor<C>,
679    {
680        Ok(self.inner.insert_many_atomic(entities)?.entities())
681    }
682
683    /// Insert a batch with explicitly non-atomic semantics.
684    ///
685    /// WARNING: fail-fast and non-atomic. Earlier inserts may commit before an error.
686    pub fn insert_many_non_atomic<E>(
687        &self,
688        entities: impl IntoIterator<Item = E>,
689    ) -> Result<Vec<E>, Error>
690    where
691        E: crate::traits::EntityFor<C>,
692    {
693        Ok(self.inner.insert_many_non_atomic(entities)?.entities())
694    }
695
696    pub fn replace<E>(&self, entity: E) -> Result<E, Error>
697    where
698        E: crate::traits::EntityFor<C>,
699    {
700        Ok(self.inner.replace(entity)?)
701    }
702
703    /// Replace a single-entity-type batch atomically in one commit window.
704    ///
705    /// If any item fails pre-commit validation, no row in the batch is persisted.
706    ///
707    /// This API is not a multi-entity transaction surface.
708    pub fn replace_many_atomic<E>(
709        &self,
710        entities: impl IntoIterator<Item = E>,
711    ) -> Result<Vec<E>, Error>
712    where
713        E: crate::traits::EntityFor<C>,
714    {
715        Ok(self.inner.replace_many_atomic(entities)?.entities())
716    }
717
718    /// Replace a batch with explicitly non-atomic semantics.
719    ///
720    /// WARNING: fail-fast and non-atomic. Earlier replaces may commit before an error.
721    pub fn replace_many_non_atomic<E>(
722        &self,
723        entities: impl IntoIterator<Item = E>,
724    ) -> Result<Vec<E>, Error>
725    where
726        E: crate::traits::EntityFor<C>,
727    {
728        Ok(self.inner.replace_many_non_atomic(entities)?.entities())
729    }
730
731    pub fn update<E>(&self, entity: E) -> Result<E, Error>
732    where
733        E: crate::traits::EntityFor<C>,
734    {
735        Ok(self.inner.update(entity)?)
736    }
737
738    /// Update one full entity and return every persisted field.
739    #[cfg(feature = "sql")]
740    pub fn update_returning_all<E>(&self, entity: E) -> Result<SqlQueryRowsOutput, Error>
741    where
742        E: crate::traits::EntityFor<C>,
743    {
744        let entity = self.inner.update(entity)?;
745
746        Self::sql_query_rows_output_from_entity::<E>(entity, None)
747    }
748
749    /// Update one full entity and return one explicit field list.
750    #[cfg(feature = "sql")]
751    pub fn update_returning<E, I, S>(
752        &self,
753        entity: E,
754        fields: I,
755    ) -> Result<SqlQueryRowsOutput, Error>
756    where
757        E: crate::traits::EntityFor<C>,
758        I: IntoIterator<Item = S>,
759        S: AsRef<str>,
760    {
761        let entity = self.inner.update(entity)?;
762        let fields = Self::returning_fields(fields);
763
764        Self::sql_query_rows_output_from_entity::<E>(entity, Some(fields.as_slice()))
765    }
766
767    /// Apply one structural mutation under one explicit write-mode contract.
768    ///
769    /// This is a dynamic, field-name-driven write ingress, not a weaker write
770    /// path: the same entity validation and commit rules still apply before
771    /// the write can succeed.
772    ///
773    /// `mode` semantics are explicit:
774    /// - `Insert`: sparse patches are allowed; missing fields must materialize
775    ///   through explicit defaults or managed-field preflight, and the write
776    ///   still fails if the row already exists.
777    /// - `Update`: patch applies over the existing row; fails if the row is missing.
778    /// - `Replace`: sparse patches are allowed, but omitted fields are not inherited
779    ///   from the previous value; they must materialize through explicit defaults
780    ///   or managed-field preflight, and the row is inserted if it is missing.
781    pub fn mutate_structural<E>(
782        &self,
783        key: E::Key,
784        patch: StructuralPatch,
785        mode: MutationMode,
786    ) -> Result<E, Error>
787    where
788        E: crate::traits::EntityFor<C>,
789    {
790        Ok(self
791            .inner
792            .mutate_structural::<E>(key, patch.inner, mode.into_core())?)
793    }
794
795    /// Build one structural mutation patch through the active accepted schema.
796    ///
797    /// This session-owned constructor resolves field names through persisted
798    /// schema metadata before returning the patch to the caller.
799    pub fn structural_patch<E, I, S>(&self, fields: I) -> Result<StructuralPatch, Error>
800    where
801        E: crate::traits::EntityFor<C>,
802        I: IntoIterator<Item = (S, InputValue)>,
803        S: AsRef<str>,
804    {
805        let fields = fields
806            .into_iter()
807            .map(|(field, value)| (field, value.into()));
808        let patch = self.inner.structural_patch::<E, _, _>(fields)?;
809
810        Ok(StructuralPatch::from_core(patch))
811    }
812
813    /// Update a single-entity-type batch atomically in one commit window.
814    ///
815    /// If any item fails pre-commit validation, no row in the batch is persisted.
816    ///
817    /// This API is not a multi-entity transaction surface.
818    pub fn update_many_atomic<E>(
819        &self,
820        entities: impl IntoIterator<Item = E>,
821    ) -> Result<Vec<E>, Error>
822    where
823        E: crate::traits::EntityFor<C>,
824    {
825        Ok(self.inner.update_many_atomic(entities)?.entities())
826    }
827
828    /// Update a batch with explicitly non-atomic semantics.
829    ///
830    /// WARNING: fail-fast and non-atomic. Earlier updates may commit before an error.
831    pub fn update_many_non_atomic<E>(
832        &self,
833        entities: impl IntoIterator<Item = E>,
834    ) -> Result<Vec<E>, Error>
835    where
836        E: crate::traits::EntityFor<C>,
837    {
838        Ok(self.inner.update_many_non_atomic(entities)?.entities())
839    }
840}
841
842///
843/// TESTS
844///
845
846#[cfg(all(test, feature = "sql", feature = "diagnostics"))]
847mod tests {
848    use super::finalize_public_sql_query_attribution;
849    use crate::db::SqlQueryExecutionAttribution;
850
851    #[test]
852    #[expect(
853        clippy::field_reassign_with_default,
854        reason = "the public diagnostics DTO test intentionally stays resilient to future attribution fields"
855    )]
856    fn public_sql_perf_attribution_total_stays_exhaustive_after_decode_finalize() {
857        let mut attribution = SqlQueryExecutionAttribution::default();
858        attribution.compile_local_instructions = 11;
859        attribution.compile.cache_lookup_local_instructions = 1;
860        attribution.compile.parse_local_instructions = 2;
861        attribution.compile.parse_tokenize_local_instructions = 1;
862        attribution.compile.parse_select_local_instructions = 1;
863        attribution.compile.prepare_local_instructions = 3;
864        attribution.compile.lower_local_instructions = 4;
865        attribution.compile.bind_local_instructions = 1;
866        attribution.plan_lookup_local_instructions = 13;
867        attribution.execution.planner_local_instructions = 13;
868        attribution.execution.store_local_instructions = 17;
869        attribution.execution.executor_invocation_local_instructions = 17;
870        attribution.execution.executor_local_instructions = 17;
871        attribution.store_get_calls = 3;
872        attribution.execute_local_instructions = 47;
873        attribution.total_local_instructions = 58;
874
875        let finalized = finalize_public_sql_query_attribution(attribution, 19);
876
877        assert_eq!(
878            finalized.execute_local_instructions,
879            finalized
880                .execution
881                .planner_local_instructions
882                .saturating_add(finalized.execution.store_local_instructions)
883                .saturating_add(finalized.execution.executor_local_instructions)
884                .saturating_add(finalized.execution.response_finalization_local_instructions)
885                .saturating_add(finalized.response_decode_local_instructions),
886            "public SQL execute totals should include planner, store, executor, and decode work",
887        );
888        assert_eq!(
889            finalized.total_local_instructions,
890            finalized
891                .compile_local_instructions
892                .saturating_add(finalized.execute_local_instructions),
893            "public SQL total instructions should remain exhaustive across compiler, planner, store, executor, and decode",
894        );
895    }
896}