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