1use 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 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 #[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 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 #[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 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 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}