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