1mod computed;
8mod lowered;
9
10use crate::{
11 db::{
12 DbSession, MissingRowPolicy, PersistedRow, Query, QueryError,
13 data::UpdatePatch,
14 executor::{EntityAuthority, MutationMode},
15 identifiers_tail_match,
16 predicate::{CompareOp, Predicate},
17 query::{intent::StructuralQuery, plan::AccessPlannedQuery},
18 session::sql::{
19 SqlDispatchResult, SqlParsedStatement, SqlStatementRoute,
20 aggregate::parsed_requires_dedicated_sql_aggregate_lane,
21 computed_projection,
22 projection::{
23 SqlProjectionPayload, execute_sql_projection_rows_for_canister,
24 execute_sql_projection_text_rows_for_canister, projection_labels_from_fields,
25 projection_labels_from_projection_spec, sql_projection_rows_from_kernel_rows,
26 },
27 },
28 sql::lowering::{
29 LoweredBaseQueryShape, LoweredSelectShape, LoweredSqlQuery, SqlLoweringError,
30 bind_lowered_sql_query,
31 },
32 sql::parser::{
33 SqlAggregateCall, SqlAggregateKind, SqlInsertStatement, SqlProjection, SqlSelectItem,
34 SqlStatement, SqlTextFunction, SqlUpdateStatement,
35 },
36 },
37 model::{entity::resolve_field_slot, field::FieldKind},
38 traits::{CanisterKind, EntityKind, EntityValue},
39 types::Timestamp,
40 value::Value,
41};
42
43#[cfg(feature = "perf-attribution")]
44pub use lowered::LoweredSqlDispatchExecutorAttribution;
45
46#[doc(hidden)]
55pub struct GeneratedSqlDispatchAttempt {
56 entity_name: &'static str,
57 explain_order_field: Option<&'static str>,
58 result: Result<SqlDispatchResult, QueryError>,
59}
60
61impl GeneratedSqlDispatchAttempt {
62 const fn new(
64 entity_name: &'static str,
65 explain_order_field: Option<&'static str>,
66 result: Result<SqlDispatchResult, QueryError>,
67 ) -> Self {
68 Self {
69 entity_name,
70 explain_order_field,
71 result,
72 }
73 }
74
75 #[must_use]
77 pub const fn entity_name(&self) -> &'static str {
78 self.entity_name
79 }
80
81 #[must_use]
83 pub const fn explain_order_field(&self) -> Option<&'static str> {
84 self.explain_order_field
85 }
86
87 pub fn into_result(self) -> Result<SqlDispatchResult, QueryError> {
89 self.result
90 }
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub(in crate::db::session::sql) enum SqlGroupingSurface {
95 Scalar,
96 Grouped,
97}
98
99const fn unsupported_sql_grouping_message(surface: SqlGroupingSurface) -> &'static str {
100 match surface {
101 SqlGroupingSurface::Scalar => {
102 "execute_sql rejects grouped SELECT; use execute_sql_grouped(...)"
103 }
104 SqlGroupingSurface::Grouped => "execute_sql_grouped requires grouped SQL query intent",
105 }
106}
107
108fn trim_generated_query_sql_input(sql: &str) -> Result<&str, QueryError> {
111 let sql_trimmed = sql.trim();
112 if sql_trimmed.is_empty() {
113 return Err(QueryError::unsupported_query(
114 "query endpoint requires a non-empty SQL string",
115 ));
116 }
117
118 Ok(sql_trimmed)
119}
120
121fn generated_sql_entities(authorities: &[EntityAuthority]) -> Vec<String> {
124 let mut entities = Vec::with_capacity(authorities.len());
125
126 for authority in authorities {
127 entities.push(authority.model().name().to_string());
128 }
129
130 entities
131}
132
133fn sql_projection_labels_from_select_statement(
136 statement: &SqlStatement,
137) -> Result<Option<Vec<String>>, QueryError> {
138 let SqlStatement::Select(select) = statement else {
139 return Err(QueryError::invariant(
140 "SQL projection labels require SELECT statement shape",
141 ));
142 };
143 let SqlProjection::Items(items) = &select.projection else {
144 return Ok(None);
145 };
146
147 Ok(Some(
148 items
149 .iter()
150 .enumerate()
151 .map(|(index, item)| {
152 select
153 .projection_alias(index)
154 .map_or_else(|| grouped_sql_projection_item_label(item), str::to_string)
155 })
156 .collect(),
157 ))
158}
159
160fn grouped_sql_projection_item_label(item: &SqlSelectItem) -> String {
163 match item {
164 SqlSelectItem::Field(field) => field.clone(),
165 SqlSelectItem::Aggregate(aggregate) => grouped_sql_aggregate_call_label(aggregate),
166 SqlSelectItem::TextFunction(call) => {
167 format!(
168 "{}({})",
169 grouped_sql_text_function_name(call.function),
170 call.field
171 )
172 }
173 }
174}
175
176fn sql_aggregate_dispatch_label_override(statement: &SqlStatement) -> Option<String> {
179 let SqlStatement::Select(select) = statement else {
180 return None;
181 };
182
183 select.projection_alias(0).map(str::to_string)
184}
185
186fn grouped_sql_aggregate_call_label(aggregate: &SqlAggregateCall) -> String {
188 let kind = match aggregate.kind {
189 SqlAggregateKind::Count => "COUNT",
190 SqlAggregateKind::Sum => "SUM",
191 SqlAggregateKind::Avg => "AVG",
192 SqlAggregateKind::Min => "MIN",
193 SqlAggregateKind::Max => "MAX",
194 };
195
196 match aggregate.field.as_deref() {
197 Some(field) => format!("{kind}({field})"),
198 None => format!("{kind}(*)"),
199 }
200}
201
202const fn grouped_sql_text_function_name(function: SqlTextFunction) -> &'static str {
205 match function {
206 SqlTextFunction::Trim => "TRIM",
207 SqlTextFunction::Ltrim => "LTRIM",
208 SqlTextFunction::Rtrim => "RTRIM",
209 SqlTextFunction::Lower => "LOWER",
210 SqlTextFunction::Upper => "UPPER",
211 SqlTextFunction::Length => "LENGTH",
212 SqlTextFunction::Left => "LEFT",
213 SqlTextFunction::Right => "RIGHT",
214 SqlTextFunction::StartsWith => "STARTS_WITH",
215 SqlTextFunction::EndsWith => "ENDS_WITH",
216 SqlTextFunction::Contains => "CONTAINS",
217 SqlTextFunction::Position => "POSITION",
218 SqlTextFunction::Replace => "REPLACE",
219 SqlTextFunction::Substring => "SUBSTRING",
220 }
221}
222
223fn authority_for_generated_sql_route(
225 route: &SqlStatementRoute,
226 authorities: &[EntityAuthority],
227) -> Result<EntityAuthority, QueryError> {
228 let sql_entity = route.entity();
229
230 for authority in authorities {
231 if identifiers_tail_match(sql_entity, authority.model().name()) {
232 return Ok(*authority);
233 }
234 }
235
236 Err(unsupported_generated_sql_entity_error(
237 sql_entity,
238 authorities,
239 ))
240}
241
242fn unsupported_generated_sql_entity_error(
245 entity_name: &str,
246 authorities: &[EntityAuthority],
247) -> QueryError {
248 let mut supported = String::new();
249
250 for (index, authority) in authorities.iter().enumerate() {
251 if index != 0 {
252 supported.push_str(", ");
253 }
254
255 supported.push_str(authority.model().name());
256 }
257
258 QueryError::unsupported_query(format!(
259 "query endpoint does not support entity '{entity_name}'; supported: {supported}"
260 ))
261}
262
263fn ensure_sql_write_entity_matches<E>(sql_entity: &str) -> Result<(), QueryError>
266where
267 E: EntityKind,
268{
269 if identifiers_tail_match(sql_entity, E::MODEL.name()) {
270 return Ok(());
271 }
272
273 Err(QueryError::from_sql_lowering_error(
274 SqlLoweringError::EntityMismatch {
275 sql_entity: sql_entity.to_string(),
276 expected_entity: E::MODEL.name(),
277 },
278 ))
279}
280
281fn sql_write_key_from_literal<E>(value: &Value, pk_name: &str) -> Result<E::Key, QueryError>
284where
285 E: EntityKind,
286{
287 if let Some(key) = <E::Key as crate::traits::FieldValue>::from_value(value) {
288 return Ok(key);
289 }
290
291 let widened = match value {
292 Value::Int(v) if *v >= 0 => Value::Uint(v.cast_unsigned()),
293 Value::Uint(v) if i64::try_from(*v).is_ok() => Value::Int(v.cast_signed()),
294 _ => {
295 return Err(QueryError::unsupported_query(format!(
296 "SQL write primary key literal for '{pk_name}' is not compatible with entity key type"
297 )));
298 }
299 };
300
301 <E::Key as crate::traits::FieldValue>::from_value(&widened).ok_or_else(|| {
302 QueryError::unsupported_query(format!(
303 "SQL write primary key literal for '{pk_name}' is not compatible with entity key type"
304 ))
305 })
306}
307
308fn sql_write_value_for_field<E>(field_name: &str, value: &Value) -> Result<Value, QueryError>
311where
312 E: EntityKind,
313{
314 let field_slot = resolve_field_slot(E::MODEL, field_name).ok_or_else(|| {
315 QueryError::invariant("SQL write field must resolve against the target entity model")
316 })?;
317 let field_kind = E::MODEL.fields()[field_slot].kind();
318
319 let normalized = match (field_kind, value) {
320 (FieldKind::Uint, Value::Int(v)) if *v >= 0 => Value::Uint(v.cast_unsigned()),
321 (FieldKind::Int, Value::Uint(v)) if i64::try_from(*v).is_ok() => {
322 Value::Int(v.cast_signed())
323 }
324 _ => value.clone(),
325 };
326
327 Ok(normalized)
328}
329
330fn sql_write_system_timestamp_fields<E>() -> Option<(&'static str, &'static str)>
334where
335 E: EntityKind,
336{
337 if resolve_field_slot(E::MODEL, "created_at").is_some()
338 && resolve_field_slot(E::MODEL, "updated_at").is_some()
339 {
340 return Some(("created_at", "updated_at"));
341 }
342
343 None
344}
345
346impl<C: CanisterKind> DbSession<C> {
347 fn sql_write_dispatch_projection<E>(entity: E) -> Result<SqlDispatchResult, QueryError>
351 where
352 E: PersistedRow<Canister = C> + EntityValue,
353 {
354 let columns = projection_labels_from_fields(E::MODEL.fields());
357 let mut row = Vec::with_capacity(columns.len());
358
359 for index in 0..columns.len() {
362 let value = entity.get_value_by_index(index).ok_or_else(|| {
363 QueryError::invariant(
364 "SQL write dispatch projection row must include every declared field",
365 )
366 })?;
367 row.push(value);
368 }
369
370 Ok(SqlDispatchResult::Projection {
371 columns,
372 rows: vec![row],
373 row_count: 1,
374 })
375 }
376
377 fn sql_insert_patch_and_key<E>(
380 statement: &SqlInsertStatement,
381 ) -> Result<(E::Key, UpdatePatch), QueryError>
382 where
383 E: PersistedRow<Canister = C> + EntityValue,
384 {
385 let pk_name = E::MODEL.primary_key.name;
388 let Some(pk_index) = statement.columns.iter().position(|field| field == pk_name) else {
389 return Err(QueryError::unsupported_query(format!(
390 "SQL INSERT requires primary key column '{pk_name}' in this release"
391 )));
392 };
393 let pk_value = statement.values.get(pk_index).ok_or_else(|| {
394 QueryError::invariant("INSERT primary key column must align with one VALUES literal")
395 })?;
396 let key = sql_write_key_from_literal::<E>(pk_value, pk_name)?;
397
398 let mut patch = UpdatePatch::new();
401 for (field, value) in statement.columns.iter().zip(statement.values.iter()) {
402 let normalized = sql_write_value_for_field::<E>(field, value)?;
403 patch = patch
404 .set_field(E::MODEL, field, normalized)
405 .map_err(QueryError::execute)?;
406 }
407
408 if let Some((created_at, updated_at)) = sql_write_system_timestamp_fields::<E>() {
411 let now = Value::Timestamp(Timestamp::now());
412 patch = patch
413 .set_field(E::MODEL, created_at, now.clone())
414 .map_err(QueryError::execute)?;
415 patch = patch
416 .set_field(E::MODEL, updated_at, now)
417 .map_err(QueryError::execute)?;
418 }
419
420 Ok((key, patch))
421 }
422
423 fn sql_update_patch_and_key<E>(
426 statement: &SqlUpdateStatement,
427 ) -> Result<(E::Key, UpdatePatch), QueryError>
428 where
429 E: PersistedRow<Canister = C> + EntityValue,
430 {
431 let pk_name = E::MODEL.primary_key.name;
435 let Some(Predicate::Compare(compare)) = &statement.predicate else {
436 return Err(QueryError::unsupported_query(format!(
437 "SQL UPDATE requires WHERE {pk_name} = literal in this release"
438 )));
439 };
440 if compare.field() != pk_name || compare.op() != CompareOp::Eq {
441 return Err(QueryError::unsupported_query(format!(
442 "SQL UPDATE requires WHERE {pk_name} = literal in this release"
443 )));
444 }
445 let key = sql_write_key_from_literal::<E>(compare.value(), pk_name)?;
446
447 let mut patch = UpdatePatch::new();
450 for assignment in &statement.assignments {
451 if assignment.field == pk_name {
452 return Err(QueryError::unsupported_query(format!(
453 "SQL UPDATE does not allow primary key mutation for '{pk_name}' in this release"
454 )));
455 }
456 let normalized =
457 sql_write_value_for_field::<E>(assignment.field.as_str(), &assignment.value)?;
458
459 patch = patch
460 .set_field(E::MODEL, assignment.field.as_str(), normalized)
461 .map_err(QueryError::execute)?;
462 }
463
464 if let Some((_, updated_at)) = sql_write_system_timestamp_fields::<E>() {
467 patch = patch
468 .set_field(E::MODEL, updated_at, Value::Timestamp(Timestamp::now()))
469 .map_err(QueryError::execute)?;
470 }
471
472 Ok((key, patch))
473 }
474
475 fn execute_sql_insert_dispatch<E>(
478 &self,
479 statement: &SqlInsertStatement,
480 ) -> Result<SqlDispatchResult, QueryError>
481 where
482 E: PersistedRow<Canister = C> + EntityValue,
483 {
484 ensure_sql_write_entity_matches::<E>(statement.entity.as_str())?;
485 let (key, patch) = Self::sql_insert_patch_and_key::<E>(statement)?;
486 let entity = self
487 .mutate_structural::<E>(key, patch, MutationMode::Insert)
488 .map_err(QueryError::execute)?;
489
490 Self::sql_write_dispatch_projection(entity)
491 }
492
493 fn execute_sql_update_dispatch<E>(
496 &self,
497 statement: &SqlUpdateStatement,
498 ) -> Result<SqlDispatchResult, QueryError>
499 where
500 E: PersistedRow<Canister = C> + EntityValue,
501 {
502 ensure_sql_write_entity_matches::<E>(statement.entity.as_str())?;
503 let (key, patch) = Self::sql_update_patch_and_key::<E>(statement)?;
504 let entity = self
505 .mutate_structural::<E>(key, patch, MutationMode::Update)
506 .map_err(QueryError::execute)?;
507
508 Self::sql_write_dispatch_projection(entity)
509 }
510
511 fn prepare_structural_sql_projection_execution(
514 &self,
515 query: StructuralQuery,
516 authority: EntityAuthority,
517 ) -> Result<(Vec<String>, AccessPlannedQuery), QueryError> {
518 let (_, plan) =
521 self.build_structural_plan_with_visible_indexes_for_authority(query, authority)?;
522 let projection = plan.projection_spec(authority.model());
523 let columns = projection_labels_from_projection_spec(&projection);
524
525 Ok((columns, plan))
526 }
527
528 pub(in crate::db::session::sql) fn execute_structural_sql_projection(
532 &self,
533 query: StructuralQuery,
534 authority: EntityAuthority,
535 ) -> Result<SqlProjectionPayload, QueryError> {
536 let (columns, plan) = self.prepare_structural_sql_projection_execution(query, authority)?;
538
539 let projected =
542 execute_sql_projection_rows_for_canister(&self.db, self.debug, authority, plan)
543 .map_err(QueryError::execute)?;
544 let (rows, row_count) = projected.into_parts();
545
546 Ok(SqlProjectionPayload::new(columns, rows, row_count))
547 }
548
549 fn execute_structural_sql_projection_text(
553 &self,
554 query: StructuralQuery,
555 authority: EntityAuthority,
556 ) -> Result<SqlDispatchResult, QueryError> {
557 let (columns, plan) = self.prepare_structural_sql_projection_execution(query, authority)?;
559
560 let projected =
563 execute_sql_projection_text_rows_for_canister(&self.db, self.debug, authority, plan)
564 .map_err(QueryError::execute)?;
565 let (rows, row_count) = projected.into_parts();
566
567 Ok(SqlDispatchResult::ProjectionText {
568 columns,
569 rows,
570 row_count,
571 })
572 }
573
574 fn execute_typed_sql_delete<E>(&self, query: &Query<E>) -> Result<SqlDispatchResult, QueryError>
578 where
579 E: PersistedRow<Canister = C> + EntityValue,
580 {
581 let plan = self
582 .compile_query_with_visible_indexes(query)?
583 .into_prepared_execution_plan();
584 let deleted = self
585 .with_metrics(|| {
586 self.delete_executor::<E>()
587 .execute_structural_projection(plan)
588 })
589 .map_err(QueryError::execute)?;
590 let (rows, row_count) = deleted.into_parts();
591 let rows = sql_projection_rows_from_kernel_rows(rows).map_err(QueryError::execute)?;
592
593 Ok(SqlProjectionPayload::new(
594 projection_labels_from_fields(E::MODEL.fields()),
595 rows,
596 row_count,
597 )
598 .into_dispatch_result())
599 }
600
601 fn lowered_sql_query_dispatch_inputs_for_authority(
604 parsed: &SqlParsedStatement,
605 authority: EntityAuthority,
606 unsupported_message: &'static str,
607 ) -> Result<(LoweredSqlQuery, Option<Vec<String>>), QueryError> {
608 let lowered = parsed.lower_query_lane_for_entity(
609 authority.model().name(),
610 authority.model().primary_key.name,
611 )?;
612 let projection_columns = matches!(lowered.query(), Some(LoweredSqlQuery::Select(_)))
613 .then(|| sql_projection_labels_from_select_statement(&parsed.statement))
614 .transpose()?;
615 let query = lowered
616 .into_query()
617 .ok_or_else(|| QueryError::unsupported_query(unsupported_message))?;
618
619 Ok((query, projection_columns.flatten()))
620 }
621
622 fn dispatch_sql_query_route_for_authority(
626 &self,
627 parsed: &SqlParsedStatement,
628 authority: EntityAuthority,
629 unsupported_message: &'static str,
630 dispatch_select: impl FnOnce(
631 &Self,
632 LoweredSelectShape,
633 EntityAuthority,
634 bool,
635 Option<Vec<String>>,
636 ) -> Result<SqlDispatchResult, QueryError>,
637 dispatch_delete: impl FnOnce(
638 &Self,
639 LoweredBaseQueryShape,
640 EntityAuthority,
641 ) -> Result<SqlDispatchResult, QueryError>,
642 ) -> Result<SqlDispatchResult, QueryError> {
643 if parsed_requires_dedicated_sql_aggregate_lane(parsed) {
646 let command =
647 Self::compile_sql_aggregate_command_core_for_authority(parsed, authority)?;
648
649 return self.execute_sql_aggregate_dispatch_for_authority(
650 command,
651 authority,
652 sql_aggregate_dispatch_label_override(&parsed.statement),
653 );
654 }
655
656 if let Some(plan) = computed_projection::computed_sql_projection_plan(&parsed.statement)? {
657 return self.execute_computed_sql_projection_dispatch_for_authority(plan, authority);
658 }
659
660 let (query, projection_columns) = Self::lowered_sql_query_dispatch_inputs_for_authority(
663 parsed,
664 authority,
665 unsupported_message,
666 )?;
667 let grouped_surface = query.has_grouping();
668
669 match query {
670 LoweredSqlQuery::Select(select) => {
671 dispatch_select(self, select, authority, grouped_surface, projection_columns)
672 }
673 LoweredSqlQuery::Delete(delete) => dispatch_delete(self, delete, authority),
674 }
675 }
676
677 fn dispatch_sql_explain_route_for_authority(
681 &self,
682 parsed: &SqlParsedStatement,
683 authority: EntityAuthority,
684 ) -> Result<SqlDispatchResult, QueryError> {
685 if let Some((mode, plan)) =
688 computed_projection::computed_sql_projection_explain_plan(&parsed.statement)?
689 {
690 return self
691 .explain_computed_sql_projection_dispatch_for_authority(mode, plan, authority)
692 .map(SqlDispatchResult::Explain);
693 }
694
695 let lowered = parsed.lower_query_lane_for_entity(
698 authority.model().name(),
699 authority.model().primary_key.name,
700 )?;
701 if let Some(explain) =
702 self.explain_lowered_sql_execution_for_authority(&lowered, authority)?
703 {
704 return Ok(SqlDispatchResult::Explain(explain));
705 }
706
707 self.explain_lowered_sql_for_authority(&lowered, authority)
708 .map(SqlDispatchResult::Explain)
709 }
710
711 pub(in crate::db::session::sql) fn ensure_sql_query_grouping<E>(
714 query: &Query<E>,
715 surface: SqlGroupingSurface,
716 ) -> Result<(), QueryError>
717 where
718 E: EntityKind,
719 {
720 match (surface, query.has_grouping()) {
721 (SqlGroupingSurface::Scalar, false) | (SqlGroupingSurface::Grouped, true) => Ok(()),
722 (SqlGroupingSurface::Scalar, true) | (SqlGroupingSurface::Grouped, false) => Err(
723 QueryError::unsupported_query(unsupported_sql_grouping_message(surface)),
724 ),
725 }
726 }
727
728 pub fn execute_sql_dispatch<E>(&self, sql: &str) -> Result<SqlDispatchResult, QueryError>
730 where
731 E: PersistedRow<Canister = C> + EntityValue,
732 {
733 let parsed = self.parse_sql_statement(sql)?;
734
735 self.execute_sql_dispatch_parsed::<E>(&parsed)
736 }
737
738 pub fn execute_sql_dispatch_parsed<E>(
740 &self,
741 parsed: &SqlParsedStatement,
742 ) -> Result<SqlDispatchResult, QueryError>
743 where
744 E: PersistedRow<Canister = C> + EntityValue,
745 {
746 match parsed.route() {
747 SqlStatementRoute::Query { .. } => self.dispatch_sql_query_route_for_authority(
748 parsed,
749 EntityAuthority::for_type::<E>(),
750 "execute_sql_dispatch accepts SELECT or DELETE only",
751 |session, select, authority, grouped_surface, projection_columns| {
752 if grouped_surface {
753 let columns = projection_columns.ok_or_else(|| {
754 QueryError::unsupported_query(
755 "grouped SQL dispatch requires explicit grouped projection items",
756 )
757 })?;
758
759 return session.execute_lowered_sql_grouped_dispatch_select_core(
760 select, authority, columns,
761 );
762 }
763
764 let payload = session.execute_lowered_sql_projection_core(select, authority)?;
765 if let Some(columns) = projection_columns {
766 let (_, rows, row_count) = payload.into_parts();
767
768 return Ok(SqlProjectionPayload::new(columns, rows, row_count)
769 .into_dispatch_result());
770 }
771
772 Ok(payload.into_dispatch_result())
773 },
774 |session, delete, _authority| {
775 let typed_query = bind_lowered_sql_query::<E>(
776 LoweredSqlQuery::Delete(delete),
777 MissingRowPolicy::Ignore,
778 )
779 .map_err(QueryError::from_sql_lowering_error)?;
780
781 session.execute_typed_sql_delete(&typed_query)
782 },
783 ),
784 SqlStatementRoute::Insert { .. } => {
785 let SqlStatement::Insert(statement) = &parsed.statement else {
786 return Err(QueryError::invariant(
787 "INSERT SQL route must carry parsed INSERT statement",
788 ));
789 };
790
791 self.execute_sql_insert_dispatch::<E>(statement)
792 }
793 SqlStatementRoute::Update { .. } => {
794 let SqlStatement::Update(statement) = &parsed.statement else {
795 return Err(QueryError::invariant(
796 "UPDATE SQL route must carry parsed UPDATE statement",
797 ));
798 };
799
800 self.execute_sql_update_dispatch::<E>(statement)
801 }
802 SqlStatementRoute::Explain { .. } => self
803 .dispatch_sql_explain_route_for_authority(parsed, EntityAuthority::for_type::<E>()),
804 SqlStatementRoute::Describe { .. } => {
805 Ok(SqlDispatchResult::Describe(self.describe_entity::<E>()))
806 }
807 SqlStatementRoute::ShowIndexes { .. } => {
808 Ok(SqlDispatchResult::ShowIndexes(self.show_indexes::<E>()))
809 }
810 SqlStatementRoute::ShowColumns { .. } => {
811 Ok(SqlDispatchResult::ShowColumns(self.show_columns::<E>()))
812 }
813 SqlStatementRoute::ShowEntities => {
814 Ok(SqlDispatchResult::ShowEntities(self.show_entities()))
815 }
816 }
817 }
818
819 #[doc(hidden)]
826 pub fn execute_generated_query_surface_dispatch_for_authority(
827 &self,
828 parsed: &SqlParsedStatement,
829 authority: EntityAuthority,
830 ) -> Result<SqlDispatchResult, QueryError> {
831 match parsed.route() {
832 SqlStatementRoute::Query { .. } => self.dispatch_sql_query_route_for_authority(
833 parsed,
834 authority,
835 "generated SQL query surface requires query or EXPLAIN statement lanes",
836 |session, select, authority, grouped_surface, projection_columns| {
837 if grouped_surface {
838 let columns = projection_columns.ok_or_else(|| {
839 QueryError::unsupported_query(
840 "grouped SQL dispatch requires explicit grouped projection items",
841 )
842 })?;
843
844 return session
845 .execute_lowered_sql_grouped_dispatch_select_core(select, authority, columns);
846 }
847
848 let result =
849 session.execute_lowered_sql_dispatch_select_text_core(select, authority)?;
850 if let Some(columns) = projection_columns {
851 let SqlDispatchResult::ProjectionText {
852 rows, row_count, ..
853 } = result
854 else {
855 return Err(QueryError::invariant(
856 "generated scalar SQL dispatch text path must emit projection text rows",
857 ));
858 };
859
860 return Ok(SqlDispatchResult::ProjectionText {
861 columns,
862 rows,
863 row_count,
864 });
865 }
866
867 Ok(result)
868 },
869 |session, delete, authority| {
870 session.execute_lowered_sql_dispatch_delete_core(&delete, authority)
871 },
872 ),
873 SqlStatementRoute::Explain { .. } => {
874 self.dispatch_sql_explain_route_for_authority(parsed, authority)
875 }
876 SqlStatementRoute::Insert { .. } | SqlStatementRoute::Update { .. }
877 | SqlStatementRoute::Describe { .. }
878 | SqlStatementRoute::ShowIndexes { .. }
879 | SqlStatementRoute::ShowColumns { .. }
880 | SqlStatementRoute::ShowEntities => Err(QueryError::unsupported_query(
881 "generated SQL query surface requires SELECT, DELETE, or EXPLAIN statement lanes",
882 )),
883 }
884 }
885
886 #[doc(hidden)]
892 #[must_use]
893 pub fn execute_generated_query_surface_sql(
894 &self,
895 sql: &str,
896 authorities: &[EntityAuthority],
897 ) -> GeneratedSqlDispatchAttempt {
898 let sql_trimmed = match trim_generated_query_sql_input(sql) {
901 Ok(sql_trimmed) => sql_trimmed,
902 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
903 };
904 let parsed = match self.parse_sql_statement(sql_trimmed) {
905 Ok(parsed) => parsed,
906 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
907 };
908
909 if matches!(parsed.route(), SqlStatementRoute::ShowEntities) {
912 return GeneratedSqlDispatchAttempt::new(
913 "",
914 None,
915 Ok(SqlDispatchResult::ShowEntities(generated_sql_entities(
916 authorities,
917 ))),
918 );
919 }
920 let authority = match authority_for_generated_sql_route(parsed.route(), authorities) {
921 Ok(authority) => authority,
922 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
923 };
924
925 let entity_name = authority.model().name();
929 let explain_order_field = parsed
930 .route()
931 .is_explain()
932 .then_some(authority.model().primary_key.name);
933 let result = match parsed.route() {
934 SqlStatementRoute::Query { .. } | SqlStatementRoute::Explain { .. } => {
935 self.execute_generated_query_surface_dispatch_for_authority(&parsed, authority)
936 }
937 SqlStatementRoute::Insert { .. } | SqlStatementRoute::Update { .. } => {
938 Err(QueryError::unsupported_query(
939 "generated SQL query surface requires SELECT, DELETE, or EXPLAIN statement lanes",
940 ))
941 }
942 SqlStatementRoute::Describe { .. } => Ok(SqlDispatchResult::Describe(
943 self.describe_entity_model(authority.model()),
944 )),
945 SqlStatementRoute::ShowIndexes { .. } => Ok(SqlDispatchResult::ShowIndexes(
946 self.show_indexes_for_store_model(authority.store_path(), authority.model()),
947 )),
948 SqlStatementRoute::ShowColumns { .. } => Ok(SqlDispatchResult::ShowColumns(
949 self.show_columns_for_model(authority.model()),
950 )),
951 SqlStatementRoute::ShowEntities => unreachable!(
952 "SHOW ENTITIES is handled before authority resolution for generated query dispatch"
953 ),
954 };
955
956 GeneratedSqlDispatchAttempt::new(entity_name, explain_order_field, result)
957 }
958}