Skip to main content

icydb_core/db/session/query/
dynamic.rs

1//! Module: db::session::query::dynamic
2//! Responsibility: lower and execute public dynamic reads against accepted schema.
3//! Does not own: query planning, accepted schema construction, or row projection.
4//! Boundary: entity-name requests converge on the shared structural read lane.
5
6use crate::{
7    db::{
8        DbSession, DynamicQuery, DynamicTypedEntityBinding, GroupedQueryOutput,
9        LiveQueryPageOutput, MissingRowPolicy, QueryError, ScalarPageWork,
10        commit::{cursor_authentication_key, database_incarnation_id},
11        cursor::{
12            CursorPlanError, ScalarOrderTermContract, ScalarPageMode, ScalarPageToken,
13            ScalarPageTokenAuthority, ScalarPageTokenProgress, ScalarPageTokenWindow,
14            decode_optional_cursor_token, encode_cursor,
15        },
16        executor::{
17            CoveringProjectionMetricsRecorder, PageWorkEnvelope,
18            ProjectionMaterializationMetricsRecorder, ScalarContinuationContext,
19            StructuralProjectionRequest, execute_structural_projection_page,
20        },
21        query::{
22            admission::{QueryAdmissionPolicy, QueryAdmissionSummary},
23            expr::{FilterExpr, OrderTerm as FluentOrderTerm},
24            intent::{IntentError, StructuralQuery},
25        },
26        session::AcceptedSchemaCatalogContext,
27    },
28    traits::CanisterKind,
29};
30use icydb_diagnostic_code::{
31    DiagnosticDecodeReason, DiagnosticExecutionBudgetResource, DiagnosticExecutionLane,
32    QueryReadAdmissionCode,
33};
34
35#[cfg(not(test))]
36const SCALAR_PAGE_OUTPUT_ROWS: usize = 1_024;
37#[cfg(test)]
38const SCALAR_PAGE_OUTPUT_ROWS: usize = 2;
39
40#[derive(Clone, Copy)]
41enum DynamicReadLane {
42    Public,
43    Trusted,
44}
45
46struct ScalarLiveCursorContract {
47    signature: crate::db::cursor::ContinuationSignature,
48    authority: ScalarPageTokenAuthority,
49    window: ScalarPageTokenWindow,
50    order_terms: Vec<ScalarOrderTermContract>,
51}
52
53impl<C: CanisterKind> DbSession<C> {
54    fn may_select_exact_single_primary_key(
55        request: &DynamicQuery,
56        catalog: &AcceptedSchemaCatalogContext,
57    ) -> bool {
58        let [primary_key] = catalog.accepted_schema_info().primary_key_names() else {
59            return false;
60        };
61        matches!(
62            request.filter_expr(),
63            Some(FilterExpr::Eq { field, .. } | FilterExpr::In { field, .. })
64                if field.eq_ignore_ascii_case(primary_key)
65        )
66    }
67
68    fn exact_primary_key_candidate_bound(
69        prepared_plan: &crate::db::executor::SharedPreparedExecutionPlan,
70    ) -> Option<usize> {
71        let access = &prepared_plan.logical_plan().access;
72        if access.as_by_key_path().is_some() {
73            return Some(1);
74        }
75
76        access.as_by_keys_path().map(<[crate::value::Value]>::len)
77    }
78
79    fn structural_query_from_dynamic_request(
80        request: &DynamicQuery,
81        catalog: &AcceptedSchemaCatalogContext,
82    ) -> Result<StructuralQuery, QueryError> {
83        Self::structural_query_from_dynamic_request_with_page_limit(request, catalog, None, false)
84    }
85
86    fn structural_query_from_dynamic_request_with_page_limit(
87        request: &DynamicQuery,
88        catalog: &AcceptedSchemaCatalogContext,
89        page_limit: Option<u32>,
90        require_total_order: bool,
91    ) -> Result<StructuralQuery, QueryError> {
92        let schema = catalog.accepted_schema_info();
93        let mut query = StructuralQuery::new(MissingRowPolicy::Ignore);
94        if let Some(filter) = request.filter_expr() {
95            query = query.filter_for_schema(schema, filter.clone());
96        }
97        for order in request.order_terms() {
98            query = query.order_term(order.clone());
99        }
100        if require_total_order && request.order_terms().is_empty() {
101            for primary_key in schema.primary_key_names() {
102                query = query.order_term(FluentOrderTerm::asc(primary_key.clone()));
103            }
104        }
105        if !request.selected_fields().is_empty() {
106            query = query.select_fields(request.selected_fields().iter().cloned());
107        }
108        if let Some(limit) = page_limit.or_else(|| request.row_limit()) {
109            query = query.limit(limit);
110        }
111        for field in request.group_fields() {
112            query = query.group_by_with_schema(field, schema)?;
113        }
114        for aggregate in request.aggregates() {
115            query = query.aggregate(aggregate.clone());
116        }
117        if let Some((max_groups, max_group_bytes)) = request.grouped_execution_limits() {
118            if max_groups == 0 || max_group_bytes == 0 {
119                return Err(QueryReadAdmissionCode::GroupedQueryRequiresLimits.into());
120            }
121            query = query.grouped_limits(u64::from(max_groups), u64::from(max_group_bytes));
122        }
123
124        Ok(query)
125    }
126
127    fn scalar_page_cursor_error() -> QueryError {
128        QueryError::from_cursor_plan_error(CursorPlanError::invalid_continuation_cursor_payload(
129            DiagnosticDecodeReason::CursorTokenDecode,
130        ))
131    }
132
133    fn scalar_live_cursor_contract(
134        request: &DynamicQuery,
135        catalog: &AcceptedSchemaCatalogContext,
136        envelope: PageWorkEnvelope,
137        prepared_plan: &crate::db::executor::SharedPreparedExecutionPlan,
138    ) -> Result<ScalarLiveCursorContract, QueryError> {
139        let signature = prepared_plan
140            .continuation_signature_for_runtime()
141            .map_err(QueryError::execute)?;
142        let root_identity = catalog.runtime_root_identity();
143        let (root_fingerprint_method, root_fingerprint) = root_identity.fingerprint();
144        let authority = ScalarPageTokenAuthority::new(
145            database_incarnation_id()
146                .map_err(QueryError::execute)?
147                .to_bytes(),
148            root_identity.accepted_root_revision().get(),
149            root_fingerprint_method,
150            root_fingerprint,
151            catalog.fingerprint(),
152            prepared_plan.authority_ref().entity_tag(),
153        );
154        let window = ScalarPageTokenWindow::new(0, request.row_limit(), envelope.identity());
155        let canonical_order = prepared_plan
156            .logical_plan()
157            .scalar_plan()
158            .order
159            .as_ref()
160            .ok_or_else(Self::scalar_page_cursor_error)?;
161        let order_terms = canonical_order
162            .fields
163            .iter()
164            .map(|term| ScalarOrderTermContract::new(term.rendered_label(), term.direction()))
165            .collect::<Vec<_>>();
166
167        Ok(ScalarLiveCursorContract {
168            signature,
169            authority,
170            window,
171            order_terms,
172        })
173    }
174
175    fn validate_scalar_page_token(
176        token: &ScalarPageToken,
177        mode: ScalarPageMode,
178        signature: crate::db::cursor::ContinuationSignature,
179        authority: ScalarPageTokenAuthority,
180        window: ScalarPageTokenWindow,
181        order_terms: &[ScalarOrderTermContract],
182        entity: &str,
183    ) -> Result<(), QueryError> {
184        if token.signature() != signature {
185            return Err(QueryError::from_cursor_plan_error(
186                CursorPlanError::continuation_cursor_signature_mismatch(
187                    entity,
188                    &signature,
189                    &token.signature(),
190                ),
191            ));
192        }
193        if token.mode() != mode
194            || token.authority() != authority
195            || token.window() != window
196            || token.order_terms() != order_terms
197        {
198            return Err(Self::scalar_page_cursor_error());
199        }
200
201        Ok(())
202    }
203
204    fn execute_dynamic_grouped_query_against_catalog(
205        &self,
206        request: &DynamicQuery,
207        lane: DynamicReadLane,
208        catalog: AcceptedSchemaCatalogContext,
209    ) -> Result<GroupedQueryOutput, QueryError> {
210        if !request.has_grouping() {
211            return Err(QueryError::intent(
212                IntentError::grouped_terminal_requires_grouped_query(),
213            ));
214        }
215        if request.grouped_execution_limits().is_none() {
216            return Err(QueryReadAdmissionCode::GroupedQueryRequiresLimits.into());
217        }
218        if !request.selected_fields().is_empty() {
219            return Err(QueryError::intent(
220                IntentError::grouped_output_defined_by_group_and_aggregates(),
221            ));
222        }
223        let query = Self::structural_query_from_dynamic_request(request, &catalog)?;
224        let public_admission = match lane {
225            DynamicReadLane::Public => Some(QueryAdmissionPolicy::default_bounded_read()),
226            DynamicReadLane::Trusted => None,
227        };
228
229        self.execute_structural_grouped_from_query(
230            &query,
231            &catalog,
232            public_admission.as_ref(),
233            request.continuation_cursor(),
234        )
235    }
236
237    #[expect(
238        clippy::too_many_lines,
239        reason = "live-page orchestration keeps planning, cursor validation, execution, and response proof in one auditable boundary"
240    )]
241    fn execute_live_page_against_catalog(
242        &self,
243        request: &DynamicQuery,
244        continuation: Option<&str>,
245        lane: DynamicReadLane,
246        catalog: AcceptedSchemaCatalogContext,
247    ) -> Result<LiveQueryPageOutput, QueryError> {
248        if request.has_grouping()
249            || request.grouped_execution_limits().is_some()
250            || request.continuation_cursor().is_some()
251        {
252            return Err(QueryError::intent(
253                IntentError::scalar_terminal_requires_scalar_query(),
254            ));
255        }
256
257        let envelope = match lane {
258            DynamicReadLane::Public => PageWorkEnvelope::public_scalar(),
259            DynamicReadLane::Trusted => PageWorkEnvelope::default_scalar(),
260        };
261        let page_row_limit = envelope
262            .limit(DiagnosticExecutionBudgetResource::ResultRows)
263            .and_then(|limit| usize::try_from(limit).ok())
264            .unwrap_or(SCALAR_PAGE_OUTPUT_ROWS)
265            .min(SCALAR_PAGE_OUTPUT_ROWS);
266        let decoded_token = decode_optional_cursor_token(continuation)
267            .map_err(QueryError::from_cursor_plan_error)?
268            .map(|bytes| {
269                ScalarPageToken::decode(
270                    bytes.as_slice(),
271                    &cursor_authentication_key().map_err(QueryError::execute)?,
272                )
273                .map_err(|error| {
274                    QueryError::from_cursor_plan_error(CursorPlanError::from_token_wire_error(
275                        error,
276                    ))
277                })
278            })
279            .transpose()?;
280        let prior_rows_emitted = decoded_token
281            .as_ref()
282            .map_or(0, |token| token.progress().rows_emitted());
283        let remaining_limit = request
284            .row_limit()
285            .map(|limit| u64::from(limit).saturating_sub(prior_rows_emitted));
286        let page_output_limit = remaining_limit
287            .unwrap_or(page_row_limit as u64)
288            .min(page_row_limit as u64);
289        let page_output_limit = usize::try_from(page_output_limit).unwrap_or(page_row_limit);
290        let execution_limit = u32::try_from(page_row_limit).unwrap_or(u32::MAX);
291        let execution_lane = match lane {
292            DynamicReadLane::Public => DiagnosticExecutionLane::PublicRead,
293            DynamicReadLane::Trusted => DiagnosticExecutionLane::TrustedRead,
294        };
295        let exact_candidate =
296            decoded_token.is_none() && Self::may_select_exact_single_primary_key(request, &catalog);
297        let initial_plan = if exact_candidate {
298            let query = Self::structural_query_from_dynamic_request(request, &catalog)?;
299            Some(
300                self.structural_projection_prepared_plan_for_accepted_authority(
301                    &query,
302                    catalog.accepted_entity_authority(),
303                    catalog.snapshot(),
304                    execution_lane,
305                )?,
306            )
307        } else {
308            None
309        };
310        let initial_is_exact_exhaustion =
311            initial_plan.as_ref().is_some_and(|(prepared_plan, _, _)| {
312                Self::exact_primary_key_candidate_bound(prepared_plan)
313                    .is_some_and(|bound| bound <= page_output_limit)
314            });
315        let (prepared_plan, projection, _) = if initial_is_exact_exhaustion {
316            initial_plan.ok_or_else(Self::scalar_page_cursor_error)?
317        } else {
318            let query = Self::structural_query_from_dynamic_request_with_page_limit(
319                request,
320                &catalog,
321                Some(execution_limit),
322                true,
323            )?;
324            self.structural_projection_prepared_plan_for_accepted_authority(
325                &query,
326                catalog.accepted_entity_authority(),
327                catalog.snapshot(),
328                execution_lane,
329            )?
330        };
331        if matches!(lane, DynamicReadLane::Public) {
332            let policy = QueryAdmissionPolicy::default_bounded_read();
333            let summary = policy.evaluate(QueryAdmissionSummary::from_plan(
334                policy.lane(),
335                prepared_plan.logical_plan(),
336            ));
337            if let Some(rejection) = summary.rejection() {
338                return Err(QueryError::from(rejection.code()));
339            }
340        }
341
342        let exact_initial_exhaustion = initial_is_exact_exhaustion;
343        let cursor_contract = decoded_token
344            .as_ref()
345            .map(|token| {
346                let contract =
347                    Self::scalar_live_cursor_contract(request, &catalog, envelope, &prepared_plan)?;
348                Self::validate_scalar_page_token(
349                    token,
350                    ScalarPageMode::Live,
351                    contract.signature,
352                    contract.authority,
353                    contract.window,
354                    contract.order_terms.as_slice(),
355                    request.entity(),
356                )?;
357                Ok::<_, QueryError>(contract)
358            })
359            .transpose()?;
360        let deferred_cursor_plan =
361            (!exact_initial_exhaustion && decoded_token.is_none()).then(|| prepared_plan.clone());
362        let continuation_context = decoded_token
363            .as_ref()
364            .and_then(|token| token.progress().last_emitted_logical().cloned())
365            .map_or_else(
366                ScalarContinuationContext::initial,
367                ScalarContinuationContext::resumed,
368            );
369        if decoded_token.is_some() && !continuation_context.has_cursor_boundary() {
370            return Err(Self::scalar_page_cursor_error());
371        }
372
373        let value_catalog = prepared_plan
374            .authority_ref()
375            .accepted_schema_info()
376            .map(crate::db::schema::SchemaInfo::value_catalog_handle)
377            .cloned()
378            .ok_or_else(QueryError::invariant)?;
379        let (columns, _fixed_scales) = projection.into_components();
380        let projection_request = StructuralProjectionRequest::new(
381            self.debug,
382            prepared_plan,
383            CoveringProjectionMetricsRecorder::none(),
384            ProjectionMaterializationMetricsRecorder::none(),
385            execution_lane,
386        );
387        let projection_request = if exact_initial_exhaustion {
388            projection_request
389        } else {
390            projection_request
391                .with_continuation(continuation_context)
392                .with_cursor_emission(page_output_limit)
393        };
394        let page = execute_structural_projection_page(&self.db, projection_request)
395            .map_err(QueryError::execute)?;
396        let row_count = page.rows.row_count();
397        let rows = page
398            .rows
399            .into_value_rows()
400            .into_iter()
401            .map(|row| {
402                row.iter()
403                    .map(|value| {
404                        crate::db::schema::output_value_from_runtime(
405                            value_catalog.enum_catalog(),
406                            value,
407                        )
408                        .map_err(|_| QueryError::invariant())
409                    })
410                    .collect::<Result<Vec<_>, _>>()
411            })
412            .collect::<Result<Vec<_>, _>>()?;
413        let rows_emitted = prior_rows_emitted.saturating_add(u64::from(row_count));
414        let total_limit_reached = request
415            .row_limit()
416            .is_some_and(|limit| rows_emitted >= u64::from(limit));
417        let continuation = if page.has_more && !total_limit_reached {
418            let logical_boundary = page
419                .last_emitted_logical
420                .ok_or_else(Self::scalar_page_cursor_error)?;
421            let cursor_contract = if let Some(contract) = cursor_contract {
422                contract
423            } else {
424                let prepared_plan = deferred_cursor_plan
425                    .as_ref()
426                    .ok_or_else(Self::scalar_page_cursor_error)?;
427                Self::scalar_live_cursor_contract(request, &catalog, envelope, prepared_plan)?
428            };
429            let token = ScalarPageToken::new(
430                ScalarPageMode::Live,
431                cursor_contract.signature,
432                cursor_contract.authority,
433                cursor_contract.window,
434                cursor_contract.order_terms,
435                ScalarPageTokenProgress::new(Some(logical_boundary), None, None, 0, rows_emitted),
436            );
437            Some(encode_cursor(
438                token
439                    .encode(&cursor_authentication_key().map_err(QueryError::execute)?)
440                    .map_err(|error| {
441                        QueryError::from_cursor_plan_error(CursorPlanError::from_token_wire_error(
442                            error,
443                        ))
444                    })?
445                    .as_slice(),
446            ))
447        } else {
448            None
449        };
450
451        Ok(LiveQueryPageOutput {
452            entity: catalog.snapshot().entity_name().to_string(),
453            columns,
454            rows,
455            row_count,
456            continuation,
457            work: ScalarPageWork {
458                envelope_identity: envelope.identity(),
459                entries_visited: page.scanned_keys as u64,
460                result_rows: row_count,
461            },
462        })
463    }
464
465    /// Execute one revision-tolerant bounded scalar page.
466    pub fn execute_public_live_page(
467        &self,
468        request: &DynamicQuery,
469        continuation: Option<&str>,
470    ) -> Result<LiveQueryPageOutput, QueryError> {
471        let catalog = self
472            .accepted_schema_catalog_context_for_entity_name(Some(request.entity()))
473            .map_err(QueryError::execute)?;
474        self.execute_live_page_against_catalog(
475            request,
476            continuation,
477            DynamicReadLane::Public,
478            catalog,
479        )
480    }
481
482    /// Execute one live page through a typed binding's immutable accepted
483    /// entity identity. `None` means the opaque binding is stale.
484    #[doc(hidden)]
485    pub fn execute_public_live_page_for_typed_binding(
486        &self,
487        binding: &DynamicTypedEntityBinding,
488        request: &DynamicQuery,
489        continuation: Option<&str>,
490    ) -> Result<Option<LiveQueryPageOutput>, QueryError> {
491        let Some(catalog) = self
492            .current_typed_entity_binding_catalog(binding)
493            .map_err(QueryError::execute)?
494        else {
495            return Ok(None);
496        };
497        self.execute_live_page_against_catalog(
498            request,
499            continuation,
500            DynamicReadLane::Public,
501            catalog,
502        )
503        .map(Some)
504    }
505
506    /// Execute one ordinary entity-name-driven bounded grouped read.
507    pub fn execute_public_dynamic_grouped_query(
508        &self,
509        request: &DynamicQuery,
510    ) -> Result<GroupedQueryOutput, QueryError> {
511        let catalog = self
512            .accepted_schema_catalog_context_for_entity_name(Some(request.entity()))
513            .map_err(QueryError::execute)?;
514        self.execute_dynamic_grouped_query_against_catalog(
515            request,
516            DynamicReadLane::Public,
517            catalog,
518        )
519    }
520
521    /// Execute one grouped typed read through the binding's immutable accepted
522    /// entity identity. `None` means the opaque binding is stale.
523    #[doc(hidden)]
524    pub fn execute_public_dynamic_grouped_query_for_typed_binding(
525        &self,
526        binding: &DynamicTypedEntityBinding,
527        request: &DynamicQuery,
528    ) -> Result<Option<GroupedQueryOutput>, QueryError> {
529        let Some(catalog) = self
530            .current_typed_entity_binding_catalog(binding)
531            .map_err(QueryError::execute)?
532        else {
533            return Ok(None);
534        };
535        self.execute_dynamic_grouped_query_against_catalog(
536            request,
537            DynamicReadLane::Public,
538            catalog,
539        )
540        .map(Some)
541    }
542
543    /// Execute one trusted entity-name-driven grouped read.
544    ///
545    /// This bypasses ordinary public admission but retains accepted-schema
546    /// planning, explicit grouped limits, cursor validation, and execution.
547    pub fn execute_trusted_dynamic_grouped_query(
548        &self,
549        request: &DynamicQuery,
550    ) -> Result<GroupedQueryOutput, QueryError> {
551        let catalog = self
552            .accepted_schema_catalog_context_for_entity_name(Some(request.entity()))
553            .map_err(QueryError::execute)?;
554        self.execute_dynamic_grouped_query_against_catalog(
555            request,
556            DynamicReadLane::Trusted,
557            catalog,
558        )
559    }
560
561    /// Execute one trusted revision-tolerant bounded dynamic page.
562    ///
563    /// Trusted execution bypasses public admission but retains the same
564    /// physical and aggregate request budgets as every other read lane.
565    pub fn execute_trusted_live_page(
566        &self,
567        request: &DynamicQuery,
568        continuation: Option<&str>,
569    ) -> Result<LiveQueryPageOutput, QueryError> {
570        let catalog = self
571            .accepted_schema_catalog_context_for_entity_name(Some(request.entity()))
572            .map_err(QueryError::execute)?;
573        self.execute_live_page_against_catalog(
574            request,
575            continuation,
576            DynamicReadLane::Trusted,
577            catalog,
578        )
579    }
580}