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