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
21pub use delete::SessionDeleteQuery;
23pub use load::{FluentLoadQuery, PagedLoadQuery};
24
25#[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#[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#[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#[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#[derive(Default)]
112pub struct StructuralPatch {
113 inner: core::db::StructuralPatch,
114}
115
116impl StructuralPatch {
117 #[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
132pub 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#[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 let (columns, indices) = Self::projection_selection::<E>(selected_fields)?;
424 let mut rows = Vec::with_capacity(entities.len());
425
426 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 #[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 #[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 #[must_use]
525 pub fn show_entities(&self) -> Vec<String> {
526 self.inner.show_entities()
527 }
528
529 #[must_use]
534 pub fn show_tables(&self) -> Vec<String> {
535 self.inner.show_tables()
536 }
537
538 #[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 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 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 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 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 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 #[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 #[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 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 #[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 #[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 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 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 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 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 #[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 #[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 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 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 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 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#[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}