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