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, SharedPreparedExecutionPlan},
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    prepared_plan: SharedPreparedExecutionPlan,
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        prepared_plan: SharedPreparedExecutionPlan,
301        columns: Vec<String>,
302        fixed_scales: Vec<Option<u32>>,
303    ) -> Self {
304        Self {
305            prepared_plan,
306            columns,
307            fixed_scales,
308        }
309    }
310
311    #[must_use]
312    pub(in crate::db) fn into_parts(
313        self,
314    ) -> (SharedPreparedExecutionPlan, Vec<String>, Vec<Option<u32>>) {
315        (self.prepared_plan, self.columns, self.fixed_scales)
316    }
317}
318
319impl SqlCompiledCommandCacheKey {
320    fn query_for_entity<E>(sql: &str) -> Self
321    where
322        E: PersistedRow + EntityValue,
323    {
324        Self::for_entity::<E>(SqlCompiledCommandSurface::Query, sql)
325    }
326
327    fn update_for_entity<E>(sql: &str) -> Self
328    where
329        E: PersistedRow + EntityValue,
330    {
331        Self::for_entity::<E>(SqlCompiledCommandSurface::Update, sql)
332    }
333
334    fn for_entity<E>(surface: SqlCompiledCommandSurface, sql: &str) -> Self
335    where
336        E: PersistedRow + EntityValue,
337    {
338        Self {
339            cache_method_version: SQL_COMPILED_COMMAND_CACHE_METHOD_VERSION,
340            surface,
341            entity_path: E::PATH,
342            schema_fingerprint: commit_schema_fingerprint_for_entity::<E>(),
343            sql: sql.to_string(),
344        }
345    }
346
347    #[must_use]
348    pub(in crate::db) const fn schema_fingerprint(&self) -> CommitSchemaFingerprint {
349        self.schema_fingerprint
350    }
351}
352
353impl SqlSelectPlanCacheKey {
354    const fn from_compiled_key(
355        compiled: SqlCompiledCommandCacheKey,
356        visibility: crate::db::session::query::QueryPlanVisibility,
357    ) -> Self {
358        Self {
359            cache_method_version: SQL_SELECT_PLAN_CACHE_METHOD_VERSION,
360            compiled,
361            visibility,
362        }
363    }
364}
365
366#[cfg(test)]
367impl SqlCompiledCommandCacheKey {
368    pub(in crate::db) fn query_for_entity_with_method_version<E>(
369        sql: &str,
370        cache_method_version: u8,
371    ) -> Self
372    where
373        E: PersistedRow + EntityValue,
374    {
375        Self::for_entity_with_method_version::<E>(
376            SqlCompiledCommandSurface::Query,
377            sql,
378            cache_method_version,
379        )
380    }
381
382    pub(in crate::db) fn update_for_entity_with_method_version<E>(
383        sql: &str,
384        cache_method_version: u8,
385    ) -> Self
386    where
387        E: PersistedRow + EntityValue,
388    {
389        Self::for_entity_with_method_version::<E>(
390            SqlCompiledCommandSurface::Update,
391            sql,
392            cache_method_version,
393        )
394    }
395
396    fn for_entity_with_method_version<E>(
397        surface: SqlCompiledCommandSurface,
398        sql: &str,
399        cache_method_version: u8,
400    ) -> Self
401    where
402        E: PersistedRow + EntityValue,
403    {
404        Self {
405            cache_method_version,
406            surface,
407            entity_path: E::PATH,
408            schema_fingerprint: commit_schema_fingerprint_for_entity::<E>(),
409            sql: sql.to_string(),
410        }
411    }
412}
413
414#[cfg(test)]
415impl SqlSelectPlanCacheKey {
416    pub(in crate::db) const fn from_compiled_key_with_method_version(
417        compiled: SqlCompiledCommandCacheKey,
418        visibility: crate::db::session::query::QueryPlanVisibility,
419        cache_method_version: u8,
420    ) -> Self {
421        Self {
422            cache_method_version,
423            compiled,
424            visibility,
425        }
426    }
427}
428
429pub(in crate::db) type SqlCompiledCommandCache =
430    HashMap<SqlCompiledCommandCacheKey, CompiledSqlCommand, CacheBuildHasher>;
431pub(in crate::db) type SqlSelectPlanCache =
432    HashMap<SqlSelectPlanCacheKey, SqlSelectPlanCacheEntry, CacheBuildHasher>;
433
434thread_local! {
435    // Keep SQL-facing caches in canister-lifetime heap state keyed by the
436    // store registry identity so update calls can warm query-facing SQL reuse
437    // without leaking entries across unrelated registries in tests.
438    static SQL_COMPILED_COMMAND_CACHES: RefCell<HashMap<usize, SqlCompiledCommandCache, CacheBuildHasher>> =
439        RefCell::new(HashMap::default());
440    static SQL_SELECT_PLAN_CACHES: RefCell<HashMap<usize, SqlSelectPlanCache, CacheBuildHasher>> =
441        RefCell::new(HashMap::default());
442}
443
444// Keep the compile artifact session-owned and generic-free so the SQL surface
445// can separate semantic compilation from execution without coupling the seam to
446// typed entity binding or executor scratch state.
447#[derive(Clone, Debug)]
448pub(in crate::db) enum CompiledSqlCommand {
449    Select {
450        query: StructuralQuery,
451        compiled_cache_key: Option<SqlCompiledCommandCacheKey>,
452    },
453    Delete {
454        query: LoweredBaseQueryShape,
455        statement: SqlDeleteStatement,
456    },
457    GlobalAggregate {
458        command: SqlGlobalAggregateCommandCore,
459        label_overrides: Vec<Option<String>>,
460    },
461    Explain(LoweredSqlCommand),
462    Insert(SqlInsertStatement),
463    Update(SqlUpdateStatement),
464    DescribeEntity,
465    ShowIndexesEntity,
466    ShowColumnsEntity,
467    ShowEntities,
468}
469
470// Keep parsing as a module-owned helper instead of hanging a pure parser off
471// `DbSession` as a fake session method.
472pub(in crate::db) fn parse_sql_statement(sql: &str) -> Result<SqlStatement, QueryError> {
473    parse_sql(sql).map_err(QueryError::from_sql_parse_error)
474}
475
476#[cfg(feature = "perf-attribution")]
477#[expect(
478    clippy::missing_const_for_fn,
479    reason = "the wasm32 branch reads the runtime performance counter and cannot be const"
480)]
481fn read_sql_local_instruction_counter() -> u64 {
482    #[cfg(target_arch = "wasm32")]
483    {
484        canic_cdk::api::performance_counter(1)
485    }
486
487    #[cfg(not(target_arch = "wasm32"))]
488    {
489        0
490    }
491}
492
493#[cfg(feature = "perf-attribution")]
494fn measure_sql_stage<T, E>(run: impl FnOnce() -> Result<T, E>) -> (u64, Result<T, E>) {
495    let start = read_sql_local_instruction_counter();
496    let result = run();
497    let delta = read_sql_local_instruction_counter().saturating_sub(start);
498
499    (delta, result)
500}
501
502impl<C: CanisterKind> DbSession<C> {
503    fn sql_cache_scope_id(&self) -> usize {
504        self.db.cache_scope_id()
505    }
506
507    fn with_sql_compiled_command_cache<R>(
508        &self,
509        f: impl FnOnce(&mut SqlCompiledCommandCache) -> R,
510    ) -> R {
511        let scope_id = self.sql_cache_scope_id();
512
513        SQL_COMPILED_COMMAND_CACHES.with(|caches| {
514            let mut caches = caches.borrow_mut();
515            let cache = caches.entry(scope_id).or_default();
516
517            f(cache)
518        })
519    }
520
521    fn with_sql_select_plan_cache<R>(&self, f: impl FnOnce(&mut SqlSelectPlanCache) -> R) -> R {
522        let scope_id = self.sql_cache_scope_id();
523
524        SQL_SELECT_PLAN_CACHES.with(|caches| {
525            let mut caches = caches.borrow_mut();
526            let cache = caches.entry(scope_id).or_default();
527
528            f(cache)
529        })
530    }
531
532    #[cfg(test)]
533    pub(in crate::db) fn sql_compiled_command_cache_len(&self) -> usize {
534        self.with_sql_compiled_command_cache(|cache| cache.len())
535    }
536
537    #[cfg(test)]
538    pub(in crate::db) fn sql_select_plan_cache_len(&self) -> usize {
539        self.with_sql_select_plan_cache(|cache| cache.len())
540    }
541
542    #[cfg(test)]
543    pub(in crate::db) fn clear_sql_caches_for_tests(&self) {
544        self.with_sql_compiled_command_cache(SqlCompiledCommandCache::clear);
545        self.with_sql_select_plan_cache(SqlSelectPlanCache::clear);
546    }
547
548    fn planned_sql_select_with_visibility(
549        &self,
550        query: &StructuralQuery,
551        authority: EntityAuthority,
552        compiled_cache_key: Option<&SqlCompiledCommandCacheKey>,
553    ) -> Result<(SqlSelectPlanCacheEntry, SqlCacheAttribution), QueryError> {
554        let visibility = self.query_plan_visibility_for_store_path(authority.store_path())?;
555        let fallback_schema_fingerprint = crate::db::schema::commit_schema_fingerprint_for_model(
556            authority.model().path,
557            authority.model(),
558        );
559        let cache_schema_fingerprint = compiled_cache_key.map_or(
560            fallback_schema_fingerprint,
561            SqlCompiledCommandCacheKey::schema_fingerprint,
562        );
563
564        let Some(compiled_cache_key) = compiled_cache_key else {
565            let (entry, cache_attribution) = self.cached_query_plan_entry_for_authority(
566                authority,
567                cache_schema_fingerprint,
568                query,
569            )?;
570            let projection = entry.logical_plan().projection_spec(authority.model());
571            let columns = projection_labels_from_projection_spec(&projection);
572            let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
573
574            return Ok((
575                SqlSelectPlanCacheEntry::new(entry.prepared_plan().clone(), columns, fixed_scales),
576                SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
577            ));
578        };
579
580        let plan_cache_key =
581            SqlSelectPlanCacheKey::from_compiled_key(compiled_cache_key.clone(), visibility);
582        {
583            let cached =
584                self.with_sql_select_plan_cache(|cache| cache.get(&plan_cache_key).cloned());
585            if let Some(plan) = cached {
586                return Ok((plan, SqlCacheAttribution::sql_select_plan_cache_hit()));
587            }
588        }
589
590        let (entry, cache_attribution) =
591            self.cached_query_plan_entry_for_authority(authority, cache_schema_fingerprint, query)?;
592        let projection = entry.logical_plan().projection_spec(authority.model());
593        let columns = projection_labels_from_projection_spec(&projection);
594        let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
595        let entry =
596            SqlSelectPlanCacheEntry::new(entry.prepared_plan().clone(), columns, fixed_scales);
597        self.with_sql_select_plan_cache(|cache| {
598            cache.insert(plan_cache_key, entry.clone());
599        });
600
601        Ok((
602            entry,
603            SqlCacheAttribution::sql_select_plan_cache_miss().merge(
604                SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
605            ),
606        ))
607    }
608
609    // Resolve planner-visible indexes and build one execution-ready
610    // structural plan at the session SQL boundary.
611    pub(in crate::db::session::sql) fn build_structural_plan_with_visible_indexes_for_authority(
612        &self,
613        query: StructuralQuery,
614        authority: EntityAuthority,
615    ) -> Result<(VisibleIndexes<'_>, AccessPlannedQuery), QueryError> {
616        let visible_indexes =
617            self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
618        let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
619
620        Ok((visible_indexes, plan))
621    }
622
623    // Keep the public SQL query surface aligned with its name and with
624    // query-shaped canister entrypoints.
625    fn ensure_sql_query_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
626        match statement {
627            SqlStatement::Select(_)
628            | SqlStatement::Explain(_)
629            | SqlStatement::Describe(_)
630            | SqlStatement::ShowIndexes(_)
631            | SqlStatement::ShowColumns(_)
632            | SqlStatement::ShowEntities(_) => Ok(()),
633            SqlStatement::Insert(_) => Err(QueryError::unsupported_query(
634                "execute_sql_query rejects INSERT; use execute_sql_update::<E>()",
635            )),
636            SqlStatement::Update(_) => Err(QueryError::unsupported_query(
637                "execute_sql_query rejects UPDATE; use execute_sql_update::<E>()",
638            )),
639            SqlStatement::Delete(_) => Err(QueryError::unsupported_query(
640                "execute_sql_query rejects DELETE; use execute_sql_update::<E>()",
641            )),
642        }
643    }
644
645    // Keep the public SQL mutation surface aligned with state-changing SQL
646    // while preserving one explicit read/introspection owner.
647    fn ensure_sql_update_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
648        match statement {
649            SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => Ok(()),
650            SqlStatement::Select(_) => Err(QueryError::unsupported_query(
651                "execute_sql_update rejects SELECT; use execute_sql_query::<E>()",
652            )),
653            SqlStatement::Explain(_) => Err(QueryError::unsupported_query(
654                "execute_sql_update rejects EXPLAIN; use execute_sql_query::<E>()",
655            )),
656            SqlStatement::Describe(_) => Err(QueryError::unsupported_query(
657                "execute_sql_update rejects DESCRIBE; use execute_sql_query::<E>()",
658            )),
659            SqlStatement::ShowIndexes(_) => Err(QueryError::unsupported_query(
660                "execute_sql_update rejects SHOW INDEXES; use execute_sql_query::<E>()",
661            )),
662            SqlStatement::ShowColumns(_) => Err(QueryError::unsupported_query(
663                "execute_sql_update rejects SHOW COLUMNS; use execute_sql_query::<E>()",
664            )),
665            SqlStatement::ShowEntities(_) => Err(QueryError::unsupported_query(
666                "execute_sql_update rejects SHOW ENTITIES; use execute_sql_query::<E>()",
667            )),
668        }
669    }
670
671    /// Execute one single-entity reduced SQL query or introspection statement.
672    ///
673    /// This surface stays hard-bound to `E`, rejects state-changing SQL, and
674    /// returns SQL-shaped statement output instead of typed entities.
675    pub fn execute_sql_query<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
676    where
677        E: PersistedRow<Canister = C> + EntityValue,
678    {
679        let compiled = self.compile_sql_query::<E>(sql)?;
680
681        self.execute_compiled_sql::<E>(&compiled)
682    }
683
684    /// Execute one reduced SQL query while reporting the compile/execute split
685    /// at the top-level SQL seam.
686    #[cfg(feature = "perf-attribution")]
687    #[doc(hidden)]
688    pub fn execute_sql_query_with_attribution<E>(
689        &self,
690        sql: &str,
691    ) -> Result<(SqlStatementResult, SqlQueryExecutionAttribution), QueryError>
692    where
693        E: PersistedRow<Canister = C> + EntityValue,
694    {
695        // Phase 1: measure the compile side of the new seam, including parse,
696        // surface validation, and semantic command construction.
697        let (compile_local_instructions, compiled) =
698            measure_sql_stage(|| self.compile_sql_query_with_cache_attribution::<E>(sql));
699        let (compiled, compile_cache_attribution) = compiled?;
700
701        // Phase 2: measure the execute side separately so repeat-run cache
702        // experiments can prove which side actually moved.
703        let store_get_calls_before = DataStore::current_get_call_count();
704        let pure_covering_decode_before = current_pure_covering_decode_local_instructions();
705        let pure_covering_row_assembly_before =
706            current_pure_covering_row_assembly_local_instructions();
707        let (result, execute_cache_attribution, execute_phase_attribution) =
708            self.execute_compiled_sql_with_phase_attribution::<E>(&compiled)?;
709        let store_get_calls =
710            DataStore::current_get_call_count().saturating_sub(store_get_calls_before);
711        let pure_covering_decode_local_instructions =
712            current_pure_covering_decode_local_instructions()
713                .saturating_sub(pure_covering_decode_before);
714        let pure_covering_row_assembly_local_instructions =
715            current_pure_covering_row_assembly_local_instructions()
716                .saturating_sub(pure_covering_row_assembly_before);
717        let execute_local_instructions = execute_phase_attribution
718            .planner_local_instructions
719            .saturating_add(execute_phase_attribution.store_local_instructions)
720            .saturating_add(execute_phase_attribution.executor_local_instructions);
721        let cache_attribution = compile_cache_attribution.merge(execute_cache_attribution);
722        let total_local_instructions =
723            compile_local_instructions.saturating_add(execute_local_instructions);
724
725        Ok((
726            result,
727            SqlQueryExecutionAttribution {
728                compile_local_instructions,
729                planner_local_instructions: execute_phase_attribution.planner_local_instructions,
730                store_local_instructions: execute_phase_attribution.store_local_instructions,
731                executor_local_instructions: execute_phase_attribution.executor_local_instructions,
732                pure_covering_decode_local_instructions,
733                pure_covering_row_assembly_local_instructions,
734                store_get_calls,
735                response_decode_local_instructions: 0,
736                execute_local_instructions,
737                total_local_instructions,
738                sql_compiled_command_cache_hits: cache_attribution.sql_compiled_command_cache_hits,
739                sql_compiled_command_cache_misses: cache_attribution
740                    .sql_compiled_command_cache_misses,
741                sql_select_plan_cache_hits: cache_attribution.sql_select_plan_cache_hits,
742                sql_select_plan_cache_misses: cache_attribution.sql_select_plan_cache_misses,
743                shared_query_plan_cache_hits: cache_attribution.shared_query_plan_cache_hits,
744                shared_query_plan_cache_misses: cache_attribution.shared_query_plan_cache_misses,
745            },
746        ))
747    }
748
749    /// Execute one single-entity reduced SQL mutation statement.
750    ///
751    /// This surface stays hard-bound to `E`, rejects read-only SQL, and
752    /// returns SQL-shaped mutation output such as counts or `RETURNING` rows.
753    pub fn execute_sql_update<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
754    where
755        E: PersistedRow<Canister = C> + EntityValue,
756    {
757        let compiled = self.compile_sql_update::<E>(sql)?;
758
759        self.execute_compiled_sql::<E>(&compiled)
760    }
761
762    #[cfg(test)]
763    pub(in crate::db) fn execute_grouped_sql_query_for_tests<E>(
764        &self,
765        sql: &str,
766        cursor_token: Option<&str>,
767    ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
768    where
769        E: PersistedRow<Canister = C> + EntityValue,
770    {
771        let parsed = parse_sql_statement(sql)?;
772
773        let lowered = lower_sql_command_from_prepared_statement(
774            prepare_sql_statement(parsed, E::MODEL.name())
775                .map_err(QueryError::from_sql_lowering_error)?,
776            E::MODEL,
777        )
778        .map_err(QueryError::from_sql_lowering_error)?;
779        let Some(query) = lowered.query().cloned() else {
780            return Err(QueryError::unsupported_query(
781                "grouped SELECT helper requires grouped SELECT",
782            ));
783        };
784        let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
785            .map_err(QueryError::from_sql_lowering_error)?;
786        if !query.has_grouping() {
787            return Err(QueryError::unsupported_query(
788                "grouped SELECT helper requires grouped SELECT",
789            ));
790        }
791
792        self.execute_grouped(&query, cursor_token)
793    }
794
795    // Compile one SQL query-surface string into the session-owned generic-free
796    // semantic command artifact before execution.
797    pub(in crate::db) fn compile_sql_query<E>(
798        &self,
799        sql: &str,
800    ) -> Result<CompiledSqlCommand, QueryError>
801    where
802        E: PersistedRow<Canister = C> + EntityValue,
803    {
804        self.compile_sql_query_with_cache_attribution::<E>(sql)
805            .map(|(compiled, _)| compiled)
806    }
807
808    fn compile_sql_query_with_cache_attribution<E>(
809        &self,
810        sql: &str,
811    ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
812    where
813        E: PersistedRow<Canister = C> + EntityValue,
814    {
815        self.compile_sql_statement_with_cache::<E>(
816            SqlCompiledCommandCacheKey::query_for_entity::<E>(sql),
817            sql,
818            Self::ensure_sql_query_statement_supported,
819        )
820    }
821
822    // Compile one SQL update-surface string into the session-owned generic-free
823    // semantic command artifact before execution.
824    pub(in crate::db) fn compile_sql_update<E>(
825        &self,
826        sql: &str,
827    ) -> Result<CompiledSqlCommand, QueryError>
828    where
829        E: PersistedRow<Canister = C> + EntityValue,
830    {
831        self.compile_sql_update_with_cache_attribution::<E>(sql)
832            .map(|(compiled, _)| compiled)
833    }
834
835    fn compile_sql_update_with_cache_attribution<E>(
836        &self,
837        sql: &str,
838    ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
839    where
840        E: PersistedRow<Canister = C> + EntityValue,
841    {
842        self.compile_sql_statement_with_cache::<E>(
843            SqlCompiledCommandCacheKey::update_for_entity::<E>(sql),
844            sql,
845            Self::ensure_sql_update_statement_supported,
846        )
847    }
848
849    // Reuse one previously compiled SQL artifact when the session-local cache
850    // can prove the surface, entity contract, and raw SQL text all match.
851    fn compile_sql_statement_with_cache<E>(
852        &self,
853        cache_key: SqlCompiledCommandCacheKey,
854        sql: &str,
855        ensure_surface_supported: fn(&SqlStatement) -> Result<(), QueryError>,
856    ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
857    where
858        E: PersistedRow<Canister = C> + EntityValue,
859    {
860        {
861            let cached =
862                self.with_sql_compiled_command_cache(|cache| cache.get(&cache_key).cloned());
863            if let Some(compiled) = cached {
864                return Ok((
865                    compiled,
866                    SqlCacheAttribution::sql_compiled_command_cache_hit(),
867                ));
868            }
869        }
870
871        let parsed = parse_sql_statement(sql)?;
872        ensure_surface_supported(&parsed)?;
873        let mut compiled = Self::compile_sql_statement_inner::<E>(&parsed)?;
874        if let CompiledSqlCommand::Select {
875            compiled_cache_key, ..
876        } = &mut compiled
877        {
878            *compiled_cache_key = Some(cache_key.clone());
879        }
880
881        self.with_sql_compiled_command_cache(|cache| {
882            cache.insert(cache_key, compiled.clone());
883        });
884
885        Ok((
886            compiled,
887            SqlCacheAttribution::sql_compiled_command_cache_miss(),
888        ))
889    }
890
891    // Compile one already-parsed SQL statement into the session-owned semantic
892    // command artifact used by the explicit compile -> execute seam.
893    pub(in crate::db) fn compile_sql_statement_inner<E>(
894        sql_statement: &SqlStatement,
895    ) -> Result<CompiledSqlCommand, QueryError>
896    where
897        E: PersistedRow<Canister = C> + EntityValue,
898    {
899        Self::compile_sql_statement_for_authority(sql_statement, EntityAuthority::for_type::<E>())
900    }
901}