Skip to main content

icydb_core/db/session/sql/
mod.rs

1//! Module: db::session::sql
2//! Responsibility: session-owned SQL execution, explain, projection, and
3//! surface-classification helpers above lowered SQL commands.
4//! Does not own: SQL parsing or structural executor runtime behavior.
5//! Boundary: keeps session visibility, authority selection, and SQL surface routing in one subsystem.
6
7mod execute;
8mod explain;
9mod projection;
10
11#[cfg(feature = "perf-attribution")]
12use candid::CandidType;
13#[cfg(feature = "perf-attribution")]
14use serde::Deserialize;
15use std::{cell::RefCell, collections::HashMap};
16
17use crate::db::sql::parser::{SqlDeleteStatement, SqlInsertStatement, SqlUpdateStatement};
18use crate::{
19    db::{
20        DbSession, GroupedRow, PersistedRow, QueryError,
21        commit::CommitSchemaFingerprint,
22        executor::EntityAuthority,
23        query::{
24            intent::StructuralQuery,
25            plan::{AccessPlannedQuery, VisibleIndexes},
26        },
27        schema::commit_schema_fingerprint_for_entity,
28        session::query::QueryPlanCacheAttribution,
29        session::sql::projection::projection_labels_from_projection_spec,
30        sql::lowering::{LoweredBaseQueryShape, LoweredSqlCommand, SqlGlobalAggregateCommandCore},
31        sql::parser::{SqlStatement, parse_sql},
32    },
33    traits::{CanisterKind, EntityValue},
34};
35
36#[cfg(test)]
37use crate::db::{
38    MissingRowPolicy, PagedGroupedExecutionWithTrace,
39    sql::lowering::{
40        bind_lowered_sql_query, lower_sql_command_from_prepared_statement, prepare_sql_statement,
41    },
42};
43
44#[cfg(feature = "structural-read-metrics")]
45pub use crate::db::session::sql::projection::{
46    SqlProjectionMaterializationMetrics, with_sql_projection_materialization_metrics,
47};
48
49/// Unified SQL statement payload returned by shared SQL lane execution.
50#[derive(Debug)]
51pub enum SqlStatementResult {
52    Count {
53        row_count: u32,
54    },
55    Projection {
56        columns: Vec<String>,
57        rows: Vec<Vec<crate::value::Value>>,
58        row_count: u32,
59    },
60    ProjectionText {
61        columns: Vec<String>,
62        rows: Vec<Vec<String>>,
63        row_count: u32,
64    },
65    Grouped {
66        columns: Vec<String>,
67        rows: Vec<GroupedRow>,
68        row_count: u32,
69        next_cursor: Option<String>,
70    },
71    Explain(String),
72    Describe(crate::db::EntitySchemaDescription),
73    ShowIndexes(Vec<String>),
74    ShowColumns(Vec<crate::db::EntityFieldDescription>),
75    ShowEntities(Vec<String>),
76}
77
78///
79/// SqlQueryExecutionAttribution
80///
81/// SqlQueryExecutionAttribution records the top-level reduced SQL query cost
82/// split at the new compile/execute seam.
83/// This keeps future cache validation focused on one concrete question:
84/// whether repeated queries stop paying compile cost while execute cost stays
85/// otherwise comparable.
86///
87
88#[cfg(feature = "perf-attribution")]
89#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
90pub struct SqlQueryExecutionAttribution {
91    pub compile_local_instructions: u64,
92    pub planner_local_instructions: u64,
93    pub executor_local_instructions: u64,
94    pub execute_local_instructions: u64,
95    pub total_local_instructions: u64,
96    pub sql_compiled_command_cache_hits: u64,
97    pub sql_compiled_command_cache_misses: u64,
98    pub sql_select_plan_cache_hits: u64,
99    pub sql_select_plan_cache_misses: u64,
100    pub shared_query_plan_cache_hits: u64,
101    pub shared_query_plan_cache_misses: u64,
102}
103
104// SqlExecutePhaseAttribution keeps the execute side split into select-plan
105// work versus narrower runtime execution so shell tooling can show both.
106#[cfg(feature = "perf-attribution")]
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108pub(in crate::db) struct SqlExecutePhaseAttribution {
109    pub planner_local_instructions: u64,
110    pub executor_local_instructions: u64,
111}
112
113#[cfg(feature = "perf-attribution")]
114impl SqlExecutePhaseAttribution {
115    #[must_use]
116    pub(in crate::db) const fn from_execute_total(execute_local_instructions: u64) -> Self {
117        Self {
118            planner_local_instructions: 0,
119            executor_local_instructions: execute_local_instructions,
120        }
121    }
122}
123
124// SqlCacheAttribution keeps the SQL-specific upper cache layers separate from
125// the shared lower query-plan cache so perf audits can tell which boundary
126// actually produced reuse on one query path.
127#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
128pub(in crate::db) struct SqlCacheAttribution {
129    pub sql_compiled_command_cache_hits: u64,
130    pub sql_compiled_command_cache_misses: u64,
131    pub sql_select_plan_cache_hits: u64,
132    pub sql_select_plan_cache_misses: u64,
133    pub shared_query_plan_cache_hits: u64,
134    pub shared_query_plan_cache_misses: u64,
135}
136
137impl SqlCacheAttribution {
138    #[must_use]
139    const fn none() -> Self {
140        Self {
141            sql_compiled_command_cache_hits: 0,
142            sql_compiled_command_cache_misses: 0,
143            sql_select_plan_cache_hits: 0,
144            sql_select_plan_cache_misses: 0,
145            shared_query_plan_cache_hits: 0,
146            shared_query_plan_cache_misses: 0,
147        }
148    }
149
150    #[must_use]
151    const fn sql_compiled_command_cache_hit() -> Self {
152        Self {
153            sql_compiled_command_cache_hits: 1,
154            ..Self::none()
155        }
156    }
157
158    #[must_use]
159    const fn sql_compiled_command_cache_miss() -> Self {
160        Self {
161            sql_compiled_command_cache_misses: 1,
162            ..Self::none()
163        }
164    }
165
166    #[must_use]
167    const fn sql_select_plan_cache_hit() -> Self {
168        Self {
169            sql_select_plan_cache_hits: 1,
170            ..Self::none()
171        }
172    }
173
174    #[must_use]
175    const fn sql_select_plan_cache_miss() -> Self {
176        Self {
177            sql_select_plan_cache_misses: 1,
178            ..Self::none()
179        }
180    }
181
182    #[must_use]
183    const fn from_shared_query_plan_cache(attribution: QueryPlanCacheAttribution) -> Self {
184        Self {
185            shared_query_plan_cache_hits: attribution.hits,
186            shared_query_plan_cache_misses: attribution.misses,
187            ..Self::none()
188        }
189    }
190
191    #[must_use]
192    const fn merge(self, other: Self) -> Self {
193        Self {
194            sql_compiled_command_cache_hits: self
195                .sql_compiled_command_cache_hits
196                .saturating_add(other.sql_compiled_command_cache_hits),
197            sql_compiled_command_cache_misses: self
198                .sql_compiled_command_cache_misses
199                .saturating_add(other.sql_compiled_command_cache_misses),
200            sql_select_plan_cache_hits: self
201                .sql_select_plan_cache_hits
202                .saturating_add(other.sql_select_plan_cache_hits),
203            sql_select_plan_cache_misses: self
204                .sql_select_plan_cache_misses
205                .saturating_add(other.sql_select_plan_cache_misses),
206            shared_query_plan_cache_hits: self
207                .shared_query_plan_cache_hits
208                .saturating_add(other.shared_query_plan_cache_hits),
209            shared_query_plan_cache_misses: self
210                .shared_query_plan_cache_misses
211                .saturating_add(other.shared_query_plan_cache_misses),
212        }
213    }
214}
215
216#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
217enum SqlCompiledCommandSurface {
218    Query,
219    Update,
220}
221
222///
223/// SqlCompiledCommandCacheKey
224///
225/// SqlCompiledCommandCacheKey pins one compiled SQL artifact to the exact
226/// session-local semantic boundary that produced it.
227/// The key is intentionally conservative: surface kind, entity path, schema
228/// fingerprint, and raw SQL text must all match before execution can reuse a
229/// prior compile result.
230///
231
232#[derive(Clone, Debug, Eq, Hash, PartialEq)]
233pub(in crate::db) struct SqlCompiledCommandCacheKey {
234    surface: SqlCompiledCommandSurface,
235    entity_path: &'static str,
236    schema_fingerprint: CommitSchemaFingerprint,
237    sql: String,
238}
239
240#[derive(Clone, Debug, Eq, Hash, PartialEq)]
241pub(in crate::db) struct SqlSelectPlanCacheKey {
242    compiled: SqlCompiledCommandCacheKey,
243    visibility: crate::db::session::query::QueryPlanVisibility,
244}
245
246///
247/// SqlSelectPlanCacheEntry
248///
249/// SqlSelectPlanCacheEntry keeps the session-owned SQL projection contract
250/// beside the already planned structural SELECT access plan.
251/// This lets repeated SQL execution reuse both planner output and outward
252/// column-label derivation without caching executor-owned runtime state.
253///
254
255#[derive(Clone, Debug)]
256pub(in crate::db) struct SqlSelectPlanCacheEntry {
257    plan: AccessPlannedQuery,
258    columns: Vec<String>,
259}
260
261impl SqlSelectPlanCacheEntry {
262    #[must_use]
263    pub(in crate::db) const fn new(plan: AccessPlannedQuery, columns: Vec<String>) -> Self {
264        Self { plan, columns }
265    }
266
267    #[must_use]
268    pub(in crate::db) fn into_parts(self) -> (AccessPlannedQuery, Vec<String>) {
269        (self.plan, self.columns)
270    }
271}
272
273impl SqlCompiledCommandCacheKey {
274    fn query_for_entity<E>(sql: &str) -> Self
275    where
276        E: PersistedRow + EntityValue,
277    {
278        Self::for_entity::<E>(SqlCompiledCommandSurface::Query, sql)
279    }
280
281    fn update_for_entity<E>(sql: &str) -> Self
282    where
283        E: PersistedRow + EntityValue,
284    {
285        Self::for_entity::<E>(SqlCompiledCommandSurface::Update, sql)
286    }
287
288    fn for_entity<E>(surface: SqlCompiledCommandSurface, sql: &str) -> Self
289    where
290        E: PersistedRow + EntityValue,
291    {
292        Self {
293            surface,
294            entity_path: E::PATH,
295            schema_fingerprint: commit_schema_fingerprint_for_entity::<E>(),
296            sql: sql.to_string(),
297        }
298    }
299
300    #[must_use]
301    pub(in crate::db) const fn schema_fingerprint(&self) -> CommitSchemaFingerprint {
302        self.schema_fingerprint
303    }
304}
305
306impl SqlSelectPlanCacheKey {
307    const fn from_compiled_key(
308        compiled: SqlCompiledCommandCacheKey,
309        visibility: crate::db::session::query::QueryPlanVisibility,
310    ) -> Self {
311        Self {
312            compiled,
313            visibility,
314        }
315    }
316}
317
318pub(in crate::db) type SqlCompiledCommandCache =
319    HashMap<SqlCompiledCommandCacheKey, CompiledSqlCommand>;
320pub(in crate::db) type SqlSelectPlanCache = HashMap<SqlSelectPlanCacheKey, SqlSelectPlanCacheEntry>;
321
322thread_local! {
323    // Keep SQL-facing caches in canister-lifetime heap state keyed by the
324    // store registry identity so update calls can warm query-facing SQL reuse
325    // without leaking entries across unrelated registries in tests.
326    static SQL_COMPILED_COMMAND_CACHES: RefCell<HashMap<usize, SqlCompiledCommandCache>> =
327        RefCell::new(HashMap::new());
328    static SQL_SELECT_PLAN_CACHES: RefCell<HashMap<usize, SqlSelectPlanCache>> =
329        RefCell::new(HashMap::new());
330}
331
332// Keep the compile artifact session-owned and generic-free so the SQL surface
333// can separate semantic compilation from execution without coupling the seam to
334// typed entity binding or executor scratch state.
335#[derive(Clone, Debug)]
336pub(in crate::db) enum CompiledSqlCommand {
337    Select {
338        query: StructuralQuery,
339        compiled_cache_key: Option<SqlCompiledCommandCacheKey>,
340    },
341    Delete {
342        query: LoweredBaseQueryShape,
343        statement: SqlDeleteStatement,
344    },
345    GlobalAggregate {
346        command: SqlGlobalAggregateCommandCore,
347        label_override: Option<String>,
348    },
349    Explain(LoweredSqlCommand),
350    Insert(SqlInsertStatement),
351    Update(SqlUpdateStatement),
352    DescribeEntity,
353    ShowIndexesEntity,
354    ShowColumnsEntity,
355    ShowEntities,
356}
357
358// Keep parsing as a module-owned helper instead of hanging a pure parser off
359// `DbSession` as a fake session method.
360pub(in crate::db) fn parse_sql_statement(sql: &str) -> Result<SqlStatement, QueryError> {
361    parse_sql(sql).map_err(QueryError::from_sql_parse_error)
362}
363
364#[cfg(feature = "perf-attribution")]
365#[expect(
366    clippy::missing_const_for_fn,
367    reason = "the wasm32 branch reads the runtime performance counter and cannot be const"
368)]
369fn read_sql_local_instruction_counter() -> u64 {
370    #[cfg(target_arch = "wasm32")]
371    {
372        canic_cdk::api::performance_counter(1)
373    }
374
375    #[cfg(not(target_arch = "wasm32"))]
376    {
377        0
378    }
379}
380
381#[cfg(feature = "perf-attribution")]
382fn measure_sql_stage<T, E>(run: impl FnOnce() -> Result<T, E>) -> (u64, Result<T, E>) {
383    let start = read_sql_local_instruction_counter();
384    let result = run();
385    let delta = read_sql_local_instruction_counter().saturating_sub(start);
386
387    (delta, result)
388}
389
390impl<C: CanisterKind> DbSession<C> {
391    fn sql_cache_scope_id(&self) -> usize {
392        self.db.cache_scope_id()
393    }
394
395    fn with_sql_compiled_command_cache<R>(
396        &self,
397        f: impl FnOnce(&mut SqlCompiledCommandCache) -> R,
398    ) -> R {
399        let scope_id = self.sql_cache_scope_id();
400
401        SQL_COMPILED_COMMAND_CACHES.with(|caches| {
402            let mut caches = caches.borrow_mut();
403            let cache = caches.entry(scope_id).or_default();
404
405            f(cache)
406        })
407    }
408
409    fn with_sql_select_plan_cache<R>(&self, f: impl FnOnce(&mut SqlSelectPlanCache) -> R) -> R {
410        let scope_id = self.sql_cache_scope_id();
411
412        SQL_SELECT_PLAN_CACHES.with(|caches| {
413            let mut caches = caches.borrow_mut();
414            let cache = caches.entry(scope_id).or_default();
415
416            f(cache)
417        })
418    }
419
420    #[cfg(test)]
421    pub(in crate::db) fn sql_compiled_command_cache_len(&self) -> usize {
422        self.with_sql_compiled_command_cache(|cache| cache.len())
423    }
424
425    #[cfg(test)]
426    pub(in crate::db) fn sql_select_plan_cache_len(&self) -> usize {
427        self.with_sql_select_plan_cache(|cache| cache.len())
428    }
429
430    #[cfg(test)]
431    pub(in crate::db) fn clear_sql_caches_for_tests(&self) {
432        self.with_sql_compiled_command_cache(SqlCompiledCommandCache::clear);
433        self.with_sql_select_plan_cache(SqlSelectPlanCache::clear);
434    }
435
436    fn planned_sql_select_with_visibility(
437        &self,
438        query: &StructuralQuery,
439        authority: EntityAuthority,
440        compiled_cache_key: Option<&SqlCompiledCommandCacheKey>,
441    ) -> Result<(SqlSelectPlanCacheEntry, SqlCacheAttribution), QueryError> {
442        let visibility = self.query_plan_visibility_for_store_path(authority.store_path())?;
443        let fallback_schema_fingerprint = crate::db::schema::commit_schema_fingerprint_for_model(
444            authority.model().path,
445            authority.model(),
446        );
447        let cache_schema_fingerprint = compiled_cache_key.map_or(
448            fallback_schema_fingerprint,
449            SqlCompiledCommandCacheKey::schema_fingerprint,
450        );
451
452        let Some(compiled_cache_key) = compiled_cache_key else {
453            let (entry, cache_attribution) = self.cached_query_plan_entry_for_authority(
454                authority,
455                cache_schema_fingerprint,
456                query,
457            )?;
458            let columns = projection_labels_from_projection_spec(
459                &entry.logical_plan().projection_spec(authority.model()),
460            );
461
462            return Ok((
463                SqlSelectPlanCacheEntry::new(entry.logical_plan().clone(), columns),
464                SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
465            ));
466        };
467
468        let plan_cache_key =
469            SqlSelectPlanCacheKey::from_compiled_key(compiled_cache_key.clone(), visibility);
470        {
471            let cached =
472                self.with_sql_select_plan_cache(|cache| cache.get(&plan_cache_key).cloned());
473            if let Some(plan) = cached {
474                return Ok((plan, SqlCacheAttribution::sql_select_plan_cache_hit()));
475            }
476        }
477
478        let (entry, cache_attribution) =
479            self.cached_query_plan_entry_for_authority(authority, cache_schema_fingerprint, query)?;
480        let columns = projection_labels_from_projection_spec(
481            &entry.logical_plan().projection_spec(authority.model()),
482        );
483        let entry = SqlSelectPlanCacheEntry::new(entry.logical_plan().clone(), columns);
484        self.with_sql_select_plan_cache(|cache| {
485            cache.insert(plan_cache_key, entry.clone());
486        });
487
488        Ok((
489            entry,
490            SqlCacheAttribution::sql_select_plan_cache_miss().merge(
491                SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
492            ),
493        ))
494    }
495
496    // Resolve planner-visible indexes and build one execution-ready
497    // structural plan at the session SQL boundary.
498    pub(in crate::db::session::sql) fn build_structural_plan_with_visible_indexes_for_authority(
499        &self,
500        query: StructuralQuery,
501        authority: EntityAuthority,
502    ) -> Result<(VisibleIndexes<'_>, AccessPlannedQuery), QueryError> {
503        let visible_indexes =
504            self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
505        let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
506
507        Ok((visible_indexes, plan))
508    }
509
510    // Keep the public SQL query surface aligned with its name and with
511    // query-shaped canister entrypoints.
512    fn ensure_sql_query_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
513        match statement {
514            SqlStatement::Select(_)
515            | SqlStatement::Explain(_)
516            | SqlStatement::Describe(_)
517            | SqlStatement::ShowIndexes(_)
518            | SqlStatement::ShowColumns(_)
519            | SqlStatement::ShowEntities(_) => Ok(()),
520            SqlStatement::Insert(_) => Err(QueryError::unsupported_query(
521                "execute_sql_query rejects INSERT; use execute_sql_update::<E>()",
522            )),
523            SqlStatement::Update(_) => Err(QueryError::unsupported_query(
524                "execute_sql_query rejects UPDATE; use execute_sql_update::<E>()",
525            )),
526            SqlStatement::Delete(_) => Err(QueryError::unsupported_query(
527                "execute_sql_query rejects DELETE; use execute_sql_update::<E>()",
528            )),
529        }
530    }
531
532    // Keep the public SQL mutation surface aligned with state-changing SQL
533    // while preserving one explicit read/introspection owner.
534    fn ensure_sql_update_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
535        match statement {
536            SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => Ok(()),
537            SqlStatement::Select(_) => Err(QueryError::unsupported_query(
538                "execute_sql_update rejects SELECT; use execute_sql_query::<E>()",
539            )),
540            SqlStatement::Explain(_) => Err(QueryError::unsupported_query(
541                "execute_sql_update rejects EXPLAIN; use execute_sql_query::<E>()",
542            )),
543            SqlStatement::Describe(_) => Err(QueryError::unsupported_query(
544                "execute_sql_update rejects DESCRIBE; use execute_sql_query::<E>()",
545            )),
546            SqlStatement::ShowIndexes(_) => Err(QueryError::unsupported_query(
547                "execute_sql_update rejects SHOW INDEXES; use execute_sql_query::<E>()",
548            )),
549            SqlStatement::ShowColumns(_) => Err(QueryError::unsupported_query(
550                "execute_sql_update rejects SHOW COLUMNS; use execute_sql_query::<E>()",
551            )),
552            SqlStatement::ShowEntities(_) => Err(QueryError::unsupported_query(
553                "execute_sql_update rejects SHOW ENTITIES; use execute_sql_query::<E>()",
554            )),
555        }
556    }
557
558    /// Execute one single-entity reduced SQL query or introspection statement.
559    ///
560    /// This surface stays hard-bound to `E`, rejects state-changing SQL, and
561    /// returns SQL-shaped statement output instead of typed entities.
562    pub fn execute_sql_query<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
563    where
564        E: PersistedRow<Canister = C> + EntityValue,
565    {
566        let compiled = self.compile_sql_query::<E>(sql)?;
567
568        self.execute_compiled_sql::<E>(&compiled)
569    }
570
571    /// Execute one reduced SQL query while reporting the compile/execute split
572    /// at the top-level SQL seam.
573    #[cfg(feature = "perf-attribution")]
574    #[doc(hidden)]
575    pub fn execute_sql_query_with_attribution<E>(
576        &self,
577        sql: &str,
578    ) -> Result<(SqlStatementResult, SqlQueryExecutionAttribution), QueryError>
579    where
580        E: PersistedRow<Canister = C> + EntityValue,
581    {
582        // Phase 1: measure the compile side of the new seam, including parse,
583        // surface validation, and semantic command construction.
584        let (compile_local_instructions, compiled) =
585            measure_sql_stage(|| self.compile_sql_query_with_cache_attribution::<E>(sql));
586        let (compiled, compile_cache_attribution) = compiled?;
587
588        // Phase 2: measure the execute side separately so repeat-run cache
589        // experiments can prove which side actually moved.
590        let (result, execute_cache_attribution, execute_phase_attribution) =
591            self.execute_compiled_sql_with_phase_attribution::<E>(&compiled)?;
592        let execute_local_instructions = execute_phase_attribution
593            .planner_local_instructions
594            .saturating_add(execute_phase_attribution.executor_local_instructions);
595        let cache_attribution = compile_cache_attribution.merge(execute_cache_attribution);
596        let total_local_instructions =
597            compile_local_instructions.saturating_add(execute_local_instructions);
598
599        Ok((
600            result,
601            SqlQueryExecutionAttribution {
602                compile_local_instructions,
603                planner_local_instructions: execute_phase_attribution.planner_local_instructions,
604                executor_local_instructions: execute_phase_attribution.executor_local_instructions,
605                execute_local_instructions,
606                total_local_instructions,
607                sql_compiled_command_cache_hits: cache_attribution.sql_compiled_command_cache_hits,
608                sql_compiled_command_cache_misses: cache_attribution
609                    .sql_compiled_command_cache_misses,
610                sql_select_plan_cache_hits: cache_attribution.sql_select_plan_cache_hits,
611                sql_select_plan_cache_misses: cache_attribution.sql_select_plan_cache_misses,
612                shared_query_plan_cache_hits: cache_attribution.shared_query_plan_cache_hits,
613                shared_query_plan_cache_misses: cache_attribution.shared_query_plan_cache_misses,
614            },
615        ))
616    }
617
618    /// Execute one single-entity reduced SQL mutation statement.
619    ///
620    /// This surface stays hard-bound to `E`, rejects read-only SQL, and
621    /// returns SQL-shaped mutation output such as counts or `RETURNING` rows.
622    pub fn execute_sql_update<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
623    where
624        E: PersistedRow<Canister = C> + EntityValue,
625    {
626        let compiled = self.compile_sql_update::<E>(sql)?;
627
628        self.execute_compiled_sql::<E>(&compiled)
629    }
630
631    #[cfg(test)]
632    pub(in crate::db) fn execute_grouped_sql_query_for_tests<E>(
633        &self,
634        sql: &str,
635        cursor_token: Option<&str>,
636    ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
637    where
638        E: PersistedRow<Canister = C> + EntityValue,
639    {
640        let parsed = parse_sql_statement(sql)?;
641
642        let lowered = lower_sql_command_from_prepared_statement(
643            prepare_sql_statement(parsed, E::MODEL.name())
644                .map_err(QueryError::from_sql_lowering_error)?,
645            E::MODEL.primary_key.name,
646        )
647        .map_err(QueryError::from_sql_lowering_error)?;
648        let Some(query) = lowered.query().cloned() else {
649            return Err(QueryError::unsupported_query(
650                "grouped SELECT helper requires grouped SELECT",
651            ));
652        };
653        let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
654            .map_err(QueryError::from_sql_lowering_error)?;
655        if !query.has_grouping() {
656            return Err(QueryError::unsupported_query(
657                "grouped SELECT helper requires grouped SELECT",
658            ));
659        }
660
661        self.execute_grouped(&query, cursor_token)
662    }
663
664    // Compile one SQL query-surface string into the session-owned generic-free
665    // semantic command artifact before execution.
666    pub(in crate::db) fn compile_sql_query<E>(
667        &self,
668        sql: &str,
669    ) -> Result<CompiledSqlCommand, QueryError>
670    where
671        E: PersistedRow<Canister = C> + EntityValue,
672    {
673        self.compile_sql_query_with_cache_attribution::<E>(sql)
674            .map(|(compiled, _)| compiled)
675    }
676
677    fn compile_sql_query_with_cache_attribution<E>(
678        &self,
679        sql: &str,
680    ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
681    where
682        E: PersistedRow<Canister = C> + EntityValue,
683    {
684        self.compile_sql_statement_with_cache::<E>(
685            SqlCompiledCommandCacheKey::query_for_entity::<E>(sql),
686            sql,
687            Self::ensure_sql_query_statement_supported,
688        )
689    }
690
691    // Compile one SQL update-surface string into the session-owned generic-free
692    // semantic command artifact before execution.
693    pub(in crate::db) fn compile_sql_update<E>(
694        &self,
695        sql: &str,
696    ) -> Result<CompiledSqlCommand, QueryError>
697    where
698        E: PersistedRow<Canister = C> + EntityValue,
699    {
700        self.compile_sql_update_with_cache_attribution::<E>(sql)
701            .map(|(compiled, _)| compiled)
702    }
703
704    fn compile_sql_update_with_cache_attribution<E>(
705        &self,
706        sql: &str,
707    ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
708    where
709        E: PersistedRow<Canister = C> + EntityValue,
710    {
711        self.compile_sql_statement_with_cache::<E>(
712            SqlCompiledCommandCacheKey::update_for_entity::<E>(sql),
713            sql,
714            Self::ensure_sql_update_statement_supported,
715        )
716    }
717
718    // Reuse one previously compiled SQL artifact when the session-local cache
719    // can prove the surface, entity contract, and raw SQL text all match.
720    fn compile_sql_statement_with_cache<E>(
721        &self,
722        cache_key: SqlCompiledCommandCacheKey,
723        sql: &str,
724        ensure_surface_supported: fn(&SqlStatement) -> Result<(), QueryError>,
725    ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
726    where
727        E: PersistedRow<Canister = C> + EntityValue,
728    {
729        {
730            let cached =
731                self.with_sql_compiled_command_cache(|cache| cache.get(&cache_key).cloned());
732            if let Some(compiled) = cached {
733                return Ok((
734                    compiled,
735                    SqlCacheAttribution::sql_compiled_command_cache_hit(),
736                ));
737            }
738        }
739
740        let parsed = parse_sql_statement(sql)?;
741        ensure_surface_supported(&parsed)?;
742        let mut compiled = Self::compile_sql_statement_inner::<E>(&parsed)?;
743        if let CompiledSqlCommand::Select {
744            compiled_cache_key, ..
745        } = &mut compiled
746        {
747            *compiled_cache_key = Some(cache_key.clone());
748        }
749
750        self.with_sql_compiled_command_cache(|cache| {
751            cache.insert(cache_key, compiled.clone());
752        });
753
754        Ok((
755            compiled,
756            SqlCacheAttribution::sql_compiled_command_cache_miss(),
757        ))
758    }
759
760    // Compile one already-parsed SQL statement into the session-owned semantic
761    // command artifact used by the explicit compile -> execute seam.
762    pub(in crate::db) fn compile_sql_statement_inner<E>(
763        sql_statement: &SqlStatement,
764    ) -> Result<CompiledSqlCommand, QueryError>
765    where
766        E: PersistedRow<Canister = C> + EntityValue,
767    {
768        Self::compile_sql_statement_for_authority(sql_statement, EntityAuthority::for_type::<E>())
769    }
770}