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