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