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