Skip to main content

icydb_core/db/session/sql/
mod.rs

1//! Module: db::session::sql
2//! Responsibility: session-owned SQL execution, explain, projection, and
3//! surface-classification helpers above lowered SQL commands.
4//! Does not own: SQL parsing or structural executor runtime behavior.
5//! Boundary: keeps session visibility, authority selection, and SQL surface routing in one subsystem.
6
7mod execute;
8mod explain;
9mod projection;
10
11#[cfg(feature = "diagnostics")]
12use candid::CandidType;
13use icydb_utils::Xxh3;
14#[cfg(feature = "diagnostics")]
15use serde::Deserialize;
16use std::{cell::RefCell, collections::HashMap, hash::BuildHasherDefault};
17
18type CacheBuildHasher = BuildHasherDefault<Xxh3>;
19
20// Bump these when SQL cache-key meaning changes in a way that must force
21// existing in-heap entries to miss instead of aliasing old semantics.
22const SQL_COMPILED_COMMAND_CACHE_METHOD_VERSION: u8 = 1;
23const SQL_SELECT_PLAN_CACHE_METHOD_VERSION: u8 = 1;
24
25#[cfg(feature = "diagnostics")]
26use crate::db::DataStore;
27#[cfg(feature = "diagnostics")]
28use crate::db::executor::GroupedCountAttribution;
29#[cfg(feature = "diagnostics")]
30use crate::db::session::sql::projection::{
31    current_pure_covering_decode_local_instructions,
32    current_pure_covering_row_assembly_local_instructions,
33};
34use crate::db::sql::parser::{SqlDeleteStatement, SqlInsertStatement, SqlUpdateStatement};
35use crate::{
36    db::{
37        DbSession, GroupedRow, PersistedRow, QueryError,
38        commit::CommitSchemaFingerprint,
39        executor::{EntityAuthority, SharedPreparedExecutionPlan},
40        query::{
41            intent::StructuralQuery,
42            plan::{AccessPlannedQuery, VisibleIndexes},
43        },
44        schema::commit_schema_fingerprint_for_entity,
45        session::query::QueryPlanCacheAttribution,
46        session::sql::projection::{
47            projection_fixed_scales_from_projection_spec, projection_labels_from_projection_spec,
48        },
49        sql::lowering::{LoweredBaseQueryShape, LoweredSqlCommand, SqlGlobalAggregateCommandCore},
50        sql::parser::{SqlStatement, parse_sql},
51    },
52    traits::{CanisterKind, EntityValue},
53};
54
55#[cfg(test)]
56use crate::db::{
57    MissingRowPolicy, PagedGroupedExecutionWithTrace,
58    sql::lowering::{
59        bind_lowered_sql_query, lower_sql_command_from_prepared_statement, prepare_sql_statement,
60    },
61};
62
63#[cfg(all(test, not(feature = "diagnostics")))]
64pub(crate) use crate::db::session::sql::projection::with_sql_projection_materialization_metrics;
65#[cfg(feature = "diagnostics")]
66pub use crate::db::session::sql::projection::{
67    SqlProjectionMaterializationMetrics, with_sql_projection_materialization_metrics,
68};
69
70/// Unified SQL statement payload returned by shared SQL lane execution.
71#[derive(Debug)]
72pub enum SqlStatementResult {
73    Count {
74        row_count: u32,
75    },
76    Projection {
77        columns: Vec<String>,
78        fixed_scales: Vec<Option<u32>>,
79        rows: Vec<Vec<crate::value::Value>>,
80        row_count: u32,
81    },
82    ProjectionText {
83        columns: Vec<String>,
84        rows: Vec<Vec<String>>,
85        row_count: u32,
86    },
87    Grouped {
88        columns: Vec<String>,
89        rows: Vec<GroupedRow>,
90        row_count: u32,
91        next_cursor: Option<String>,
92    },
93    Explain(String),
94    Describe(crate::db::EntitySchemaDescription),
95    ShowIndexes(Vec<String>),
96    ShowColumns(Vec<crate::db::EntityFieldDescription>),
97    ShowEntities(Vec<String>),
98}
99
100///
101/// SqlQueryExecutionAttribution
102///
103/// SqlQueryExecutionAttribution records the top-level reduced SQL query cost
104/// split at the new compile/execute seam.
105/// This keeps future cache validation focused on one concrete question:
106/// whether repeated queries stop paying compile cost while execute cost stays
107/// otherwise comparable.
108///
109
110#[cfg(feature = "diagnostics")]
111#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
112pub struct SqlQueryExecutionAttribution {
113    pub compile_local_instructions: u64,
114    pub planner_local_instructions: u64,
115    pub store_local_instructions: u64,
116    pub executor_local_instructions: u64,
117    pub grouped_stream_local_instructions: u64,
118    pub grouped_fold_local_instructions: u64,
119    pub grouped_finalize_local_instructions: u64,
120    pub grouped_count_borrowed_hash_computations: u64,
121    pub grouped_count_bucket_candidate_checks: u64,
122    pub grouped_count_existing_group_hits: u64,
123    pub grouped_count_new_group_inserts: u64,
124    pub grouped_count_row_materialization_local_instructions: u64,
125    pub grouped_count_group_lookup_local_instructions: u64,
126    pub grouped_count_existing_group_update_local_instructions: u64,
127    pub grouped_count_new_group_insert_local_instructions: u64,
128    pub pure_covering_decode_local_instructions: u64,
129    pub pure_covering_row_assembly_local_instructions: u64,
130    pub store_get_calls: u64,
131    pub response_decode_local_instructions: u64,
132    pub execute_local_instructions: u64,
133    pub total_local_instructions: u64,
134    pub sql_compiled_command_cache_hits: u64,
135    pub sql_compiled_command_cache_misses: u64,
136    pub sql_select_plan_cache_hits: u64,
137    pub sql_select_plan_cache_misses: u64,
138    pub shared_query_plan_cache_hits: u64,
139    pub shared_query_plan_cache_misses: u64,
140}
141
142// SqlExecutePhaseAttribution keeps the execute side split into select-plan
143// work, physical store/index access, and narrower runtime execution so shell
144// tooling can show all three.
145#[cfg(feature = "diagnostics")]
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147pub(in crate::db) struct SqlExecutePhaseAttribution {
148    pub planner_local_instructions: u64,
149    pub store_local_instructions: u64,
150    pub executor_local_instructions: u64,
151    pub grouped_stream_local_instructions: u64,
152    pub grouped_fold_local_instructions: u64,
153    pub grouped_finalize_local_instructions: u64,
154    pub grouped_count: GroupedCountAttribution,
155}
156
157#[cfg(feature = "diagnostics")]
158impl SqlExecutePhaseAttribution {
159    #[must_use]
160    pub(in crate::db) const fn from_execute_total_and_store_total(
161        execute_local_instructions: u64,
162        store_local_instructions: u64,
163    ) -> Self {
164        Self {
165            planner_local_instructions: 0,
166            store_local_instructions,
167            executor_local_instructions: execute_local_instructions
168                .saturating_sub(store_local_instructions),
169            grouped_stream_local_instructions: 0,
170            grouped_fold_local_instructions: 0,
171            grouped_finalize_local_instructions: 0,
172            grouped_count: GroupedCountAttribution::none(),
173        }
174    }
175}
176
177// SqlCacheAttribution keeps the SQL-specific upper cache layers separate from
178// the shared lower query-plan cache so perf audits can tell which boundary
179// actually produced reuse on one query path.
180#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
181pub(in crate::db) struct SqlCacheAttribution {
182    pub sql_compiled_command_cache_hits: u64,
183    pub sql_compiled_command_cache_misses: u64,
184    pub sql_select_plan_cache_hits: u64,
185    pub sql_select_plan_cache_misses: u64,
186    pub shared_query_plan_cache_hits: u64,
187    pub shared_query_plan_cache_misses: u64,
188}
189
190impl SqlCacheAttribution {
191    #[must_use]
192    const fn none() -> Self {
193        Self {
194            sql_compiled_command_cache_hits: 0,
195            sql_compiled_command_cache_misses: 0,
196            sql_select_plan_cache_hits: 0,
197            sql_select_plan_cache_misses: 0,
198            shared_query_plan_cache_hits: 0,
199            shared_query_plan_cache_misses: 0,
200        }
201    }
202
203    #[must_use]
204    const fn sql_compiled_command_cache_hit() -> Self {
205        Self {
206            sql_compiled_command_cache_hits: 1,
207            ..Self::none()
208        }
209    }
210
211    #[must_use]
212    const fn sql_compiled_command_cache_miss() -> Self {
213        Self {
214            sql_compiled_command_cache_misses: 1,
215            ..Self::none()
216        }
217    }
218
219    #[must_use]
220    const fn sql_select_plan_cache_hit() -> Self {
221        Self {
222            sql_select_plan_cache_hits: 1,
223            ..Self::none()
224        }
225    }
226
227    #[must_use]
228    const fn sql_select_plan_cache_miss() -> Self {
229        Self {
230            sql_select_plan_cache_misses: 1,
231            ..Self::none()
232        }
233    }
234
235    #[must_use]
236    const fn from_shared_query_plan_cache(attribution: QueryPlanCacheAttribution) -> Self {
237        Self {
238            shared_query_plan_cache_hits: attribution.hits,
239            shared_query_plan_cache_misses: attribution.misses,
240            ..Self::none()
241        }
242    }
243
244    #[must_use]
245    const fn merge(self, other: Self) -> Self {
246        Self {
247            sql_compiled_command_cache_hits: self
248                .sql_compiled_command_cache_hits
249                .saturating_add(other.sql_compiled_command_cache_hits),
250            sql_compiled_command_cache_misses: self
251                .sql_compiled_command_cache_misses
252                .saturating_add(other.sql_compiled_command_cache_misses),
253            sql_select_plan_cache_hits: self
254                .sql_select_plan_cache_hits
255                .saturating_add(other.sql_select_plan_cache_hits),
256            sql_select_plan_cache_misses: self
257                .sql_select_plan_cache_misses
258                .saturating_add(other.sql_select_plan_cache_misses),
259            shared_query_plan_cache_hits: self
260                .shared_query_plan_cache_hits
261                .saturating_add(other.shared_query_plan_cache_hits),
262            shared_query_plan_cache_misses: self
263                .shared_query_plan_cache_misses
264                .saturating_add(other.shared_query_plan_cache_misses),
265        }
266    }
267}
268
269#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
270enum SqlCompiledCommandSurface {
271    Query,
272    Update,
273}
274
275///
276/// SqlCompiledCommandCacheKey
277///
278/// SqlCompiledCommandCacheKey pins one compiled SQL artifact to the exact
279/// session-local semantic boundary that produced it.
280/// The key is intentionally conservative: surface kind, entity path, schema
281/// fingerprint, and raw SQL text must all match before execution can reuse a
282/// prior compile result.
283///
284
285#[derive(Clone, Debug, Eq, Hash, PartialEq)]
286pub(in crate::db) struct SqlCompiledCommandCacheKey {
287    cache_method_version: u8,
288    surface: SqlCompiledCommandSurface,
289    entity_path: &'static str,
290    schema_fingerprint: CommitSchemaFingerprint,
291    sql: String,
292}
293
294#[derive(Clone, Debug, Eq, Hash, PartialEq)]
295pub(in crate::db) struct SqlSelectPlanCacheKey {
296    cache_method_version: u8,
297    compiled: SqlCompiledCommandCacheKey,
298    visibility: crate::db::session::query::QueryPlanVisibility,
299}
300
301///
302/// SqlSelectPlanCacheEntry
303///
304/// SqlSelectPlanCacheEntry keeps the session-owned SQL projection contract
305/// beside the already planned structural SELECT access plan.
306/// This lets repeated SQL execution reuse both planner output and outward
307/// column-label derivation without caching executor-owned runtime state.
308///
309
310#[derive(Clone, Debug)]
311pub(in crate::db) struct SqlSelectPlanCacheEntry {
312    prepared_plan: SharedPreparedExecutionPlan,
313    columns: Vec<String>,
314    fixed_scales: Vec<Option<u32>>,
315}
316
317impl SqlSelectPlanCacheEntry {
318    #[must_use]
319    pub(in crate::db) const fn new(
320        prepared_plan: SharedPreparedExecutionPlan,
321        columns: Vec<String>,
322        fixed_scales: Vec<Option<u32>>,
323    ) -> Self {
324        Self {
325            prepared_plan,
326            columns,
327            fixed_scales,
328        }
329    }
330
331    #[must_use]
332    pub(in crate::db) fn into_parts(
333        self,
334    ) -> (SharedPreparedExecutionPlan, Vec<String>, Vec<Option<u32>>) {
335        (self.prepared_plan, self.columns, self.fixed_scales)
336    }
337}
338
339impl SqlCompiledCommandCacheKey {
340    fn query_for_entity<E>(sql: &str) -> Self
341    where
342        E: PersistedRow + EntityValue,
343    {
344        Self::for_entity::<E>(SqlCompiledCommandSurface::Query, sql)
345    }
346
347    fn update_for_entity<E>(sql: &str) -> Self
348    where
349        E: PersistedRow + EntityValue,
350    {
351        Self::for_entity::<E>(SqlCompiledCommandSurface::Update, sql)
352    }
353
354    fn for_entity<E>(surface: SqlCompiledCommandSurface, sql: &str) -> Self
355    where
356        E: PersistedRow + EntityValue,
357    {
358        Self {
359            cache_method_version: SQL_COMPILED_COMMAND_CACHE_METHOD_VERSION,
360            surface,
361            entity_path: E::PATH,
362            schema_fingerprint: commit_schema_fingerprint_for_entity::<E>(),
363            sql: sql.to_string(),
364        }
365    }
366
367    #[must_use]
368    pub(in crate::db) const fn schema_fingerprint(&self) -> CommitSchemaFingerprint {
369        self.schema_fingerprint
370    }
371}
372
373impl SqlSelectPlanCacheKey {
374    const fn from_compiled_key(
375        compiled: SqlCompiledCommandCacheKey,
376        visibility: crate::db::session::query::QueryPlanVisibility,
377    ) -> Self {
378        Self {
379            cache_method_version: SQL_SELECT_PLAN_CACHE_METHOD_VERSION,
380            compiled,
381            visibility,
382        }
383    }
384}
385
386#[cfg(test)]
387impl SqlCompiledCommandCacheKey {
388    pub(in crate::db) fn query_for_entity_with_method_version<E>(
389        sql: &str,
390        cache_method_version: u8,
391    ) -> Self
392    where
393        E: PersistedRow + EntityValue,
394    {
395        Self::for_entity_with_method_version::<E>(
396            SqlCompiledCommandSurface::Query,
397            sql,
398            cache_method_version,
399        )
400    }
401
402    pub(in crate::db) fn update_for_entity_with_method_version<E>(
403        sql: &str,
404        cache_method_version: u8,
405    ) -> Self
406    where
407        E: PersistedRow + EntityValue,
408    {
409        Self::for_entity_with_method_version::<E>(
410            SqlCompiledCommandSurface::Update,
411            sql,
412            cache_method_version,
413        )
414    }
415
416    fn for_entity_with_method_version<E>(
417        surface: SqlCompiledCommandSurface,
418        sql: &str,
419        cache_method_version: u8,
420    ) -> Self
421    where
422        E: PersistedRow + EntityValue,
423    {
424        Self {
425            cache_method_version,
426            surface,
427            entity_path: E::PATH,
428            schema_fingerprint: commit_schema_fingerprint_for_entity::<E>(),
429            sql: sql.to_string(),
430        }
431    }
432}
433
434#[cfg(test)]
435impl SqlSelectPlanCacheKey {
436    pub(in crate::db) const fn from_compiled_key_with_method_version(
437        compiled: SqlCompiledCommandCacheKey,
438        visibility: crate::db::session::query::QueryPlanVisibility,
439        cache_method_version: u8,
440    ) -> Self {
441        Self {
442            cache_method_version,
443            compiled,
444            visibility,
445        }
446    }
447}
448
449pub(in crate::db) type SqlCompiledCommandCache =
450    HashMap<SqlCompiledCommandCacheKey, CompiledSqlCommand, CacheBuildHasher>;
451pub(in crate::db) type SqlSelectPlanCache =
452    HashMap<SqlSelectPlanCacheKey, SqlSelectPlanCacheEntry, CacheBuildHasher>;
453
454thread_local! {
455    // Keep SQL-facing caches in canister-lifetime heap state keyed by the
456    // store registry identity so update calls can warm query-facing SQL reuse
457    // without leaking entries across unrelated registries in tests.
458    static SQL_COMPILED_COMMAND_CACHES: RefCell<HashMap<usize, SqlCompiledCommandCache, CacheBuildHasher>> =
459        RefCell::new(HashMap::default());
460    static SQL_SELECT_PLAN_CACHES: RefCell<HashMap<usize, SqlSelectPlanCache, CacheBuildHasher>> =
461        RefCell::new(HashMap::default());
462}
463
464// Keep the compile artifact session-owned and generic-free so the SQL surface
465// can separate semantic compilation from execution without coupling the seam to
466// typed entity binding or executor scratch state.
467#[derive(Clone, Debug)]
468pub(in crate::db) enum CompiledSqlCommand {
469    Select {
470        query: StructuralQuery,
471        compiled_cache_key: Option<SqlCompiledCommandCacheKey>,
472    },
473    Delete {
474        query: LoweredBaseQueryShape,
475        statement: SqlDeleteStatement,
476    },
477    GlobalAggregate {
478        command: SqlGlobalAggregateCommandCore,
479        label_overrides: Vec<Option<String>>,
480    },
481    Explain(LoweredSqlCommand),
482    Insert(SqlInsertStatement),
483    Update(SqlUpdateStatement),
484    DescribeEntity,
485    ShowIndexesEntity,
486    ShowColumnsEntity,
487    ShowEntities,
488}
489
490// Keep parsing as a module-owned helper instead of hanging a pure parser off
491// `DbSession` as a fake session method.
492pub(in crate::db) fn parse_sql_statement(sql: &str) -> Result<SqlStatement, QueryError> {
493    parse_sql(sql).map_err(QueryError::from_sql_parse_error)
494}
495
496#[cfg(feature = "diagnostics")]
497#[expect(
498    clippy::missing_const_for_fn,
499    reason = "the wasm32 branch reads the runtime performance counter and cannot be const"
500)]
501fn read_sql_local_instruction_counter() -> u64 {
502    #[cfg(target_arch = "wasm32")]
503    {
504        canic_cdk::api::performance_counter(1)
505    }
506
507    #[cfg(not(target_arch = "wasm32"))]
508    {
509        0
510    }
511}
512
513#[cfg(feature = "diagnostics")]
514fn measure_sql_stage<T, E>(run: impl FnOnce() -> Result<T, E>) -> (u64, Result<T, E>) {
515    let start = read_sql_local_instruction_counter();
516    let result = run();
517    let delta = read_sql_local_instruction_counter().saturating_sub(start);
518
519    (delta, result)
520}
521
522impl<C: CanisterKind> DbSession<C> {
523    fn sql_cache_scope_id(&self) -> usize {
524        self.db.cache_scope_id()
525    }
526
527    fn with_sql_compiled_command_cache<R>(
528        &self,
529        f: impl FnOnce(&mut SqlCompiledCommandCache) -> R,
530    ) -> R {
531        let scope_id = self.sql_cache_scope_id();
532
533        SQL_COMPILED_COMMAND_CACHES.with(|caches| {
534            let mut caches = caches.borrow_mut();
535            let cache = caches.entry(scope_id).or_default();
536
537            f(cache)
538        })
539    }
540
541    fn with_sql_select_plan_cache<R>(&self, f: impl FnOnce(&mut SqlSelectPlanCache) -> R) -> R {
542        let scope_id = self.sql_cache_scope_id();
543
544        SQL_SELECT_PLAN_CACHES.with(|caches| {
545            let mut caches = caches.borrow_mut();
546            let cache = caches.entry(scope_id).or_default();
547
548            f(cache)
549        })
550    }
551
552    #[cfg(test)]
553    pub(in crate::db) fn sql_compiled_command_cache_len(&self) -> usize {
554        self.with_sql_compiled_command_cache(|cache| cache.len())
555    }
556
557    #[cfg(test)]
558    pub(in crate::db) fn sql_select_plan_cache_len(&self) -> usize {
559        self.with_sql_select_plan_cache(|cache| cache.len())
560    }
561
562    #[cfg(test)]
563    pub(in crate::db) fn clear_sql_caches_for_tests(&self) {
564        self.with_sql_compiled_command_cache(SqlCompiledCommandCache::clear);
565        self.with_sql_select_plan_cache(SqlSelectPlanCache::clear);
566    }
567
568    fn planned_sql_select_with_visibility(
569        &self,
570        query: &StructuralQuery,
571        authority: EntityAuthority,
572        compiled_cache_key: Option<&SqlCompiledCommandCacheKey>,
573    ) -> Result<(SqlSelectPlanCacheEntry, SqlCacheAttribution), QueryError> {
574        let visibility = self.query_plan_visibility_for_store_path(authority.store_path())?;
575        let fallback_schema_fingerprint = crate::db::schema::commit_schema_fingerprint_for_model(
576            authority.model().path,
577            authority.model(),
578        );
579        let cache_schema_fingerprint = compiled_cache_key.map_or(
580            fallback_schema_fingerprint,
581            SqlCompiledCommandCacheKey::schema_fingerprint,
582        );
583
584        let Some(compiled_cache_key) = compiled_cache_key else {
585            let (entry, cache_attribution) = self.cached_query_plan_entry_for_authority(
586                authority,
587                cache_schema_fingerprint,
588                query,
589            )?;
590            let projection = entry.logical_plan().projection_spec(authority.model());
591            let columns = projection_labels_from_projection_spec(&projection);
592            let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
593
594            return Ok((
595                SqlSelectPlanCacheEntry::new(entry.prepared_plan().clone(), columns, fixed_scales),
596                SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
597            ));
598        };
599
600        let plan_cache_key =
601            SqlSelectPlanCacheKey::from_compiled_key(compiled_cache_key.clone(), visibility);
602        {
603            let cached =
604                self.with_sql_select_plan_cache(|cache| cache.get(&plan_cache_key).cloned());
605            if let Some(plan) = cached {
606                return Ok((plan, SqlCacheAttribution::sql_select_plan_cache_hit()));
607            }
608        }
609
610        let (entry, cache_attribution) =
611            self.cached_query_plan_entry_for_authority(authority, cache_schema_fingerprint, query)?;
612        let projection = entry.logical_plan().projection_spec(authority.model());
613        let columns = projection_labels_from_projection_spec(&projection);
614        let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
615        let entry =
616            SqlSelectPlanCacheEntry::new(entry.prepared_plan().clone(), columns, fixed_scales);
617        self.with_sql_select_plan_cache(|cache| {
618            cache.insert(plan_cache_key, entry.clone());
619        });
620
621        Ok((
622            entry,
623            SqlCacheAttribution::sql_select_plan_cache_miss().merge(
624                SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
625            ),
626        ))
627    }
628
629    // Resolve planner-visible indexes and build one execution-ready
630    // structural plan at the session SQL boundary.
631    pub(in crate::db::session::sql) fn build_structural_plan_with_visible_indexes_for_authority(
632        &self,
633        query: StructuralQuery,
634        authority: EntityAuthority,
635    ) -> Result<(VisibleIndexes<'_>, AccessPlannedQuery), QueryError> {
636        let visible_indexes =
637            self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
638        let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
639
640        Ok((visible_indexes, plan))
641    }
642
643    // Keep the public SQL query surface aligned with its name and with
644    // query-shaped canister entrypoints.
645    fn ensure_sql_query_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
646        match statement {
647            SqlStatement::Select(_)
648            | SqlStatement::Explain(_)
649            | SqlStatement::Describe(_)
650            | SqlStatement::ShowIndexes(_)
651            | SqlStatement::ShowColumns(_)
652            | SqlStatement::ShowEntities(_) => Ok(()),
653            SqlStatement::Insert(_) => Err(QueryError::unsupported_query(
654                "execute_sql_query rejects INSERT; use execute_sql_update::<E>()",
655            )),
656            SqlStatement::Update(_) => Err(QueryError::unsupported_query(
657                "execute_sql_query rejects UPDATE; use execute_sql_update::<E>()",
658            )),
659            SqlStatement::Delete(_) => Err(QueryError::unsupported_query(
660                "execute_sql_query rejects DELETE; use execute_sql_update::<E>()",
661            )),
662        }
663    }
664
665    // Keep the public SQL mutation surface aligned with state-changing SQL
666    // while preserving one explicit read/introspection owner.
667    fn ensure_sql_update_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
668        match statement {
669            SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => Ok(()),
670            SqlStatement::Select(_) => Err(QueryError::unsupported_query(
671                "execute_sql_update rejects SELECT; use execute_sql_query::<E>()",
672            )),
673            SqlStatement::Explain(_) => Err(QueryError::unsupported_query(
674                "execute_sql_update rejects EXPLAIN; use execute_sql_query::<E>()",
675            )),
676            SqlStatement::Describe(_) => Err(QueryError::unsupported_query(
677                "execute_sql_update rejects DESCRIBE; use execute_sql_query::<E>()",
678            )),
679            SqlStatement::ShowIndexes(_) => Err(QueryError::unsupported_query(
680                "execute_sql_update rejects SHOW INDEXES; use execute_sql_query::<E>()",
681            )),
682            SqlStatement::ShowColumns(_) => Err(QueryError::unsupported_query(
683                "execute_sql_update rejects SHOW COLUMNS; use execute_sql_query::<E>()",
684            )),
685            SqlStatement::ShowEntities(_) => Err(QueryError::unsupported_query(
686                "execute_sql_update rejects SHOW ENTITIES; use execute_sql_query::<E>()",
687            )),
688        }
689    }
690
691    /// Execute one single-entity reduced SQL query or introspection statement.
692    ///
693    /// This surface stays hard-bound to `E`, rejects state-changing SQL, and
694    /// returns SQL-shaped statement output instead of typed entities.
695    pub fn execute_sql_query<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
696    where
697        E: PersistedRow<Canister = C> + EntityValue,
698    {
699        let compiled = self.compile_sql_query::<E>(sql)?;
700
701        self.execute_compiled_sql::<E>(&compiled)
702    }
703
704    /// Execute one reduced SQL query while reporting the compile/execute split
705    /// at the top-level SQL seam.
706    #[cfg(feature = "diagnostics")]
707    #[doc(hidden)]
708    pub fn execute_sql_query_with_attribution<E>(
709        &self,
710        sql: &str,
711    ) -> Result<(SqlStatementResult, SqlQueryExecutionAttribution), QueryError>
712    where
713        E: PersistedRow<Canister = C> + EntityValue,
714    {
715        // Phase 1: measure the compile side of the new seam, including parse,
716        // surface validation, and semantic command construction.
717        let (compile_local_instructions, compiled) =
718            measure_sql_stage(|| self.compile_sql_query_with_cache_attribution::<E>(sql));
719        let (compiled, compile_cache_attribution) = compiled?;
720
721        // Phase 2: measure the execute side separately so repeat-run cache
722        // experiments can prove which side actually moved.
723        let store_get_calls_before = DataStore::current_get_call_count();
724        let pure_covering_decode_before = current_pure_covering_decode_local_instructions();
725        let pure_covering_row_assembly_before =
726            current_pure_covering_row_assembly_local_instructions();
727        let (result, execute_cache_attribution, execute_phase_attribution) =
728            self.execute_compiled_sql_with_phase_attribution::<E>(&compiled)?;
729        let store_get_calls =
730            DataStore::current_get_call_count().saturating_sub(store_get_calls_before);
731        let pure_covering_decode_local_instructions =
732            current_pure_covering_decode_local_instructions()
733                .saturating_sub(pure_covering_decode_before);
734        let pure_covering_row_assembly_local_instructions =
735            current_pure_covering_row_assembly_local_instructions()
736                .saturating_sub(pure_covering_row_assembly_before);
737        let execute_local_instructions = execute_phase_attribution
738            .planner_local_instructions
739            .saturating_add(execute_phase_attribution.store_local_instructions)
740            .saturating_add(execute_phase_attribution.executor_local_instructions);
741        let cache_attribution = compile_cache_attribution.merge(execute_cache_attribution);
742        let total_local_instructions =
743            compile_local_instructions.saturating_add(execute_local_instructions);
744
745        Ok((
746            result,
747            SqlQueryExecutionAttribution {
748                compile_local_instructions,
749                planner_local_instructions: execute_phase_attribution.planner_local_instructions,
750                store_local_instructions: execute_phase_attribution.store_local_instructions,
751                executor_local_instructions: execute_phase_attribution.executor_local_instructions,
752                grouped_stream_local_instructions: execute_phase_attribution
753                    .grouped_stream_local_instructions,
754                grouped_fold_local_instructions: execute_phase_attribution
755                    .grouped_fold_local_instructions,
756                grouped_finalize_local_instructions: execute_phase_attribution
757                    .grouped_finalize_local_instructions,
758                grouped_count_borrowed_hash_computations: execute_phase_attribution
759                    .grouped_count
760                    .borrowed_hash_computations,
761                grouped_count_bucket_candidate_checks: execute_phase_attribution
762                    .grouped_count
763                    .bucket_candidate_checks,
764                grouped_count_existing_group_hits: execute_phase_attribution
765                    .grouped_count
766                    .existing_group_hits,
767                grouped_count_new_group_inserts: execute_phase_attribution
768                    .grouped_count
769                    .new_group_inserts,
770                grouped_count_row_materialization_local_instructions: execute_phase_attribution
771                    .grouped_count
772                    .row_materialization_local_instructions,
773                grouped_count_group_lookup_local_instructions: execute_phase_attribution
774                    .grouped_count
775                    .group_lookup_local_instructions,
776                grouped_count_existing_group_update_local_instructions: execute_phase_attribution
777                    .grouped_count
778                    .existing_group_update_local_instructions,
779                grouped_count_new_group_insert_local_instructions: execute_phase_attribution
780                    .grouped_count
781                    .new_group_insert_local_instructions,
782                pure_covering_decode_local_instructions,
783                pure_covering_row_assembly_local_instructions,
784                store_get_calls,
785                response_decode_local_instructions: 0,
786                execute_local_instructions,
787                total_local_instructions,
788                sql_compiled_command_cache_hits: cache_attribution.sql_compiled_command_cache_hits,
789                sql_compiled_command_cache_misses: cache_attribution
790                    .sql_compiled_command_cache_misses,
791                sql_select_plan_cache_hits: cache_attribution.sql_select_plan_cache_hits,
792                sql_select_plan_cache_misses: cache_attribution.sql_select_plan_cache_misses,
793                shared_query_plan_cache_hits: cache_attribution.shared_query_plan_cache_hits,
794                shared_query_plan_cache_misses: cache_attribution.shared_query_plan_cache_misses,
795            },
796        ))
797    }
798
799    /// Execute one single-entity reduced SQL mutation statement.
800    ///
801    /// This surface stays hard-bound to `E`, rejects read-only SQL, and
802    /// returns SQL-shaped mutation output such as counts or `RETURNING` rows.
803    pub fn execute_sql_update<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
804    where
805        E: PersistedRow<Canister = C> + EntityValue,
806    {
807        let compiled = self.compile_sql_update::<E>(sql)?;
808
809        self.execute_compiled_sql::<E>(&compiled)
810    }
811
812    #[cfg(test)]
813    pub(in crate::db) fn execute_grouped_sql_query_for_tests<E>(
814        &self,
815        sql: &str,
816        cursor_token: Option<&str>,
817    ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
818    where
819        E: PersistedRow<Canister = C> + EntityValue,
820    {
821        let parsed = parse_sql_statement(sql)?;
822
823        let lowered = lower_sql_command_from_prepared_statement(
824            prepare_sql_statement(parsed, E::MODEL.name())
825                .map_err(QueryError::from_sql_lowering_error)?,
826            E::MODEL,
827        )
828        .map_err(QueryError::from_sql_lowering_error)?;
829        let Some(query) = lowered.query().cloned() else {
830            return Err(QueryError::unsupported_query(
831                "grouped SELECT helper requires grouped SELECT",
832            ));
833        };
834        let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
835            .map_err(QueryError::from_sql_lowering_error)?;
836        if !query.has_grouping() {
837            return Err(QueryError::unsupported_query(
838                "grouped SELECT helper requires grouped SELECT",
839            ));
840        }
841
842        self.execute_grouped(&query, cursor_token)
843    }
844
845    // Compile one SQL query-surface string into the session-owned generic-free
846    // semantic command artifact before execution.
847    pub(in crate::db) fn compile_sql_query<E>(
848        &self,
849        sql: &str,
850    ) -> Result<CompiledSqlCommand, QueryError>
851    where
852        E: PersistedRow<Canister = C> + EntityValue,
853    {
854        self.compile_sql_query_with_cache_attribution::<E>(sql)
855            .map(|(compiled, _)| compiled)
856    }
857
858    fn compile_sql_query_with_cache_attribution<E>(
859        &self,
860        sql: &str,
861    ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
862    where
863        E: PersistedRow<Canister = C> + EntityValue,
864    {
865        self.compile_sql_statement_with_cache::<E>(
866            SqlCompiledCommandCacheKey::query_for_entity::<E>(sql),
867            sql,
868            Self::ensure_sql_query_statement_supported,
869        )
870    }
871
872    // Compile one SQL update-surface string into the session-owned generic-free
873    // semantic command artifact before execution.
874    pub(in crate::db) fn compile_sql_update<E>(
875        &self,
876        sql: &str,
877    ) -> Result<CompiledSqlCommand, QueryError>
878    where
879        E: PersistedRow<Canister = C> + EntityValue,
880    {
881        self.compile_sql_update_with_cache_attribution::<E>(sql)
882            .map(|(compiled, _)| compiled)
883    }
884
885    fn compile_sql_update_with_cache_attribution<E>(
886        &self,
887        sql: &str,
888    ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
889    where
890        E: PersistedRow<Canister = C> + EntityValue,
891    {
892        self.compile_sql_statement_with_cache::<E>(
893            SqlCompiledCommandCacheKey::update_for_entity::<E>(sql),
894            sql,
895            Self::ensure_sql_update_statement_supported,
896        )
897    }
898
899    // Reuse one previously compiled SQL artifact when the session-local cache
900    // can prove the surface, entity contract, and raw SQL text all match.
901    fn compile_sql_statement_with_cache<E>(
902        &self,
903        cache_key: SqlCompiledCommandCacheKey,
904        sql: &str,
905        ensure_surface_supported: fn(&SqlStatement) -> Result<(), QueryError>,
906    ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
907    where
908        E: PersistedRow<Canister = C> + EntityValue,
909    {
910        {
911            let cached =
912                self.with_sql_compiled_command_cache(|cache| cache.get(&cache_key).cloned());
913            if let Some(compiled) = cached {
914                return Ok((
915                    compiled,
916                    SqlCacheAttribution::sql_compiled_command_cache_hit(),
917                ));
918            }
919        }
920
921        let parsed = parse_sql_statement(sql)?;
922        ensure_surface_supported(&parsed)?;
923        let mut compiled = Self::compile_sql_statement_inner::<E>(&parsed)?;
924        if let CompiledSqlCommand::Select {
925            compiled_cache_key, ..
926        } = &mut compiled
927        {
928            *compiled_cache_key = Some(cache_key.clone());
929        }
930
931        self.with_sql_compiled_command_cache(|cache| {
932            cache.insert(cache_key, compiled.clone());
933        });
934
935        Ok((
936            compiled,
937            SqlCacheAttribution::sql_compiled_command_cache_miss(),
938        ))
939    }
940
941    // Compile one already-parsed SQL statement into the session-owned semantic
942    // command artifact used by the explicit compile -> execute seam.
943    pub(in crate::db) fn compile_sql_statement_inner<E>(
944        sql_statement: &SqlStatement,
945    ) -> Result<CompiledSqlCommand, QueryError>
946    where
947        E: PersistedRow<Canister = C> + EntityValue,
948    {
949        Self::compile_sql_statement_for_authority(sql_statement, EntityAuthority::for_type::<E>())
950    }
951}