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