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