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