1mod computed;
7mod lowered;
8
9use crate::{
10 db::{
11 DbSession, MissingRowPolicy, PersistedRow, Query, QueryError,
12 executor::{
13 EntityAuthority, execute_sql_projection_rows_for_canister,
14 execute_sql_projection_text_rows_for_canister,
15 },
16 identifiers_tail_match,
17 query::intent::StructuralQuery,
18 session::sql::{
19 SqlDispatchResult, SqlParsedStatement, SqlStatementRoute,
20 aggregate::{
21 SqlAggregateSurface, parsed_requires_dedicated_sql_aggregate_lane,
22 unsupported_sql_aggregate_lane_message,
23 },
24 computed_projection,
25 projection::{
26 SqlProjectionPayload, projection_labels_from_entity_model,
27 projection_labels_from_projection_spec, sql_projection_rows_from_kernel_rows,
28 },
29 },
30 sql::lowering::{
31 LoweredSqlCommand, LoweredSqlQuery, bind_lowered_sql_query,
32 lower_sql_command_from_prepared_statement,
33 },
34 },
35 traits::{CanisterKind, EntityKind, EntityValue},
36};
37
38#[doc(hidden)]
47pub struct GeneratedSqlDispatchAttempt {
48 entity_name: &'static str,
49 explain_order_field: Option<&'static str>,
50 result: Result<SqlDispatchResult, QueryError>,
51}
52
53impl GeneratedSqlDispatchAttempt {
54 const fn new(
56 entity_name: &'static str,
57 explain_order_field: Option<&'static str>,
58 result: Result<SqlDispatchResult, QueryError>,
59 ) -> Self {
60 Self {
61 entity_name,
62 explain_order_field,
63 result,
64 }
65 }
66
67 #[must_use]
69 pub const fn entity_name(&self) -> &'static str {
70 self.entity_name
71 }
72
73 #[must_use]
75 pub const fn explain_order_field(&self) -> Option<&'static str> {
76 self.explain_order_field
77 }
78
79 pub fn into_result(self) -> Result<SqlDispatchResult, QueryError> {
81 self.result
82 }
83}
84
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub(in crate::db::session::sql) enum SqlGroupingSurface {
87 Scalar,
88 Dispatch,
89 GeneratedQuerySurface,
90 Grouped,
91}
92
93const fn unsupported_sql_grouping_message(surface: SqlGroupingSurface) -> &'static str {
94 match surface {
95 SqlGroupingSurface::Scalar => {
96 "execute_sql rejects grouped SELECT; use execute_sql_grouped(...)"
97 }
98 SqlGroupingSurface::Dispatch => {
99 "execute_sql_dispatch rejects grouped SELECT execution; use execute_sql_grouped(...)"
100 }
101 SqlGroupingSurface::GeneratedQuerySurface => {
102 "generated SQL query surface rejects grouped SELECT execution; 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 authority_for_generated_sql_route(
135 route: &SqlStatementRoute,
136 authorities: &[EntityAuthority],
137) -> Result<EntityAuthority, QueryError> {
138 let sql_entity = route.entity();
139
140 for authority in authorities {
141 if identifiers_tail_match(sql_entity, authority.model().name()) {
142 return Ok(*authority);
143 }
144 }
145
146 Err(unsupported_generated_sql_entity_error(
147 sql_entity,
148 authorities,
149 ))
150}
151
152fn unsupported_generated_sql_entity_error(
155 entity_name: &str,
156 authorities: &[EntityAuthority],
157) -> QueryError {
158 let mut supported = String::new();
159
160 for (index, authority) in authorities.iter().enumerate() {
161 if index != 0 {
162 supported.push_str(", ");
163 }
164
165 supported.push_str(authority.model().name());
166 }
167
168 QueryError::unsupported_query(format!(
169 "query endpoint does not support entity '{entity_name}'; supported: {supported}"
170 ))
171}
172
173impl<C: CanisterKind> DbSession<C> {
174 fn execute_structural_sql_projection(
178 &self,
179 query: StructuralQuery,
180 authority: EntityAuthority,
181 ) -> Result<SqlProjectionPayload, QueryError> {
182 let visible_indexes =
185 self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
186 let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
187 let projection = plan.projection_spec(authority.model());
188 let columns = projection_labels_from_projection_spec(&projection);
189
190 let projected = execute_sql_projection_rows_for_canister(
193 &self.db,
194 self.debug,
195 authority.model(),
196 projection,
197 authority,
198 plan,
199 )
200 .map_err(QueryError::execute)?;
201 let (rows, row_count) = projected.into_parts();
202
203 Ok(SqlProjectionPayload::new(columns, rows, row_count))
204 }
205
206 fn execute_structural_sql_projection_text(
210 &self,
211 query: StructuralQuery,
212 authority: EntityAuthority,
213 ) -> Result<SqlDispatchResult, QueryError> {
214 let visible_indexes =
217 self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
218 let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
219 let projection = plan.projection_spec(authority.model());
220 let columns = projection_labels_from_projection_spec(&projection);
221
222 let projected = execute_sql_projection_text_rows_for_canister(
225 &self.db,
226 self.debug,
227 authority.model(),
228 projection,
229 authority,
230 plan,
231 )
232 .map_err(QueryError::execute)?;
233 let (rows, row_count) = projected.into_parts();
234
235 Ok(SqlDispatchResult::ProjectionText {
236 columns,
237 rows,
238 row_count,
239 })
240 }
241
242 fn execute_typed_sql_delete<E>(&self, query: &Query<E>) -> Result<SqlDispatchResult, QueryError>
246 where
247 E: PersistedRow<Canister = C> + EntityValue,
248 {
249 let plan = self
250 .compile_query_with_visible_indexes(query)?
251 .into_executable();
252 let deleted = self
253 .with_metrics(|| self.delete_executor::<E>().execute_sql_projection(plan))
254 .map_err(QueryError::execute)?;
255 let (rows, row_count) = deleted.into_parts();
256 let rows = sql_projection_rows_from_kernel_rows(rows);
257
258 Ok(SqlProjectionPayload::new(
259 projection_labels_from_entity_model(E::MODEL),
260 rows,
261 row_count,
262 )
263 .into_dispatch_result())
264 }
265
266 pub(in crate::db::session::sql) fn ensure_sql_query_grouping<E>(
269 query: &Query<E>,
270 surface: SqlGroupingSurface,
271 ) -> Result<(), QueryError>
272 where
273 E: EntityKind,
274 {
275 match (surface, query.has_grouping()) {
276 (
277 SqlGroupingSurface::Scalar
278 | SqlGroupingSurface::Dispatch
279 | SqlGroupingSurface::GeneratedQuerySurface,
280 false,
281 )
282 | (SqlGroupingSurface::Grouped, true) => Ok(()),
283 (
284 SqlGroupingSurface::Scalar
285 | SqlGroupingSurface::Dispatch
286 | SqlGroupingSurface::GeneratedQuerySurface,
287 true,
288 )
289 | (SqlGroupingSurface::Grouped, false) => Err(QueryError::unsupported_query(
290 unsupported_sql_grouping_message(surface),
291 )),
292 }
293 }
294
295 pub(in crate::db::session::sql) fn ensure_lowered_sql_query_grouping(
298 lowered: &LoweredSqlCommand,
299 surface: SqlGroupingSurface,
300 ) -> Result<(), QueryError> {
301 let Some(query) = lowered.query() else {
302 return Ok(());
303 };
304
305 match (surface, query.has_grouping()) {
306 (
307 SqlGroupingSurface::Scalar
308 | SqlGroupingSurface::Dispatch
309 | SqlGroupingSurface::GeneratedQuerySurface,
310 false,
311 )
312 | (SqlGroupingSurface::Grouped, true) => Ok(()),
313 (
314 SqlGroupingSurface::Scalar
315 | SqlGroupingSurface::Dispatch
316 | SqlGroupingSurface::GeneratedQuerySurface,
317 true,
318 )
319 | (SqlGroupingSurface::Grouped, false) => Err(QueryError::unsupported_query(
320 unsupported_sql_grouping_message(surface),
321 )),
322 }
323 }
324
325 pub fn execute_sql_dispatch<E>(&self, sql: &str) -> Result<SqlDispatchResult, QueryError>
327 where
328 E: PersistedRow<Canister = C> + EntityValue,
329 {
330 let parsed = self.parse_sql_statement(sql)?;
331
332 self.execute_sql_dispatch_parsed::<E>(&parsed)
333 }
334
335 pub fn execute_sql_dispatch_parsed<E>(
337 &self,
338 parsed: &SqlParsedStatement,
339 ) -> Result<SqlDispatchResult, QueryError>
340 where
341 E: PersistedRow<Canister = C> + EntityValue,
342 {
343 match parsed.route() {
344 SqlStatementRoute::Query { .. } => {
345 if parsed_requires_dedicated_sql_aggregate_lane(parsed) {
346 return Err(QueryError::unsupported_query(
347 unsupported_sql_aggregate_lane_message(
348 SqlAggregateSurface::ExecuteSqlDispatch,
349 ),
350 ));
351 }
352
353 if let Some(plan) =
354 computed_projection::computed_sql_projection_plan(&parsed.statement)?
355 {
356 return self.execute_computed_sql_projection_dispatch::<E>(plan);
357 }
358
359 let lowered = parsed
363 .lower_query_lane_for_entity(E::MODEL.name(), E::MODEL.primary_key.name)?;
364
365 Self::ensure_lowered_sql_query_grouping(&lowered, SqlGroupingSurface::Dispatch)?;
366
367 match lowered.query() {
371 Some(LoweredSqlQuery::Select(select)) => self
372 .execute_lowered_sql_dispatch_select_core(
373 select,
374 EntityAuthority::for_type::<E>(),
375 ),
376 Some(LoweredSqlQuery::Delete(delete)) => {
377 let typed_query = bind_lowered_sql_query::<E>(
378 LoweredSqlQuery::Delete(delete.clone()),
379 MissingRowPolicy::Ignore,
380 )
381 .map_err(QueryError::from_sql_lowering_error)?;
382
383 self.execute_typed_sql_delete(&typed_query)
384 }
385 None => Err(QueryError::unsupported_query(
386 "execute_sql_dispatch accepts SELECT or DELETE only",
387 )),
388 }
389 }
390 SqlStatementRoute::Explain { .. } => {
391 if let Some((mode, plan)) =
392 computed_projection::computed_sql_projection_explain_plan(&parsed.statement)?
393 {
394 return self
395 .explain_computed_sql_projection_dispatch::<E>(mode, plan)
396 .map(SqlDispatchResult::Explain);
397 }
398
399 let lowered = lower_sql_command_from_prepared_statement(
400 parsed.prepare(E::MODEL.name())?,
401 E::MODEL.primary_key.name,
402 )
403 .map_err(QueryError::from_sql_lowering_error)?;
404 if let Some(explain) = self.explain_lowered_sql_execution_for_authority(
405 &lowered,
406 EntityAuthority::for_type::<E>(),
407 )? {
408 return Ok(SqlDispatchResult::Explain(explain));
409 }
410
411 self.explain_lowered_sql_for_authority(&lowered, EntityAuthority::for_type::<E>())
412 .map(SqlDispatchResult::Explain)
413 }
414 SqlStatementRoute::Describe { .. } => {
415 Ok(SqlDispatchResult::Describe(self.describe_entity::<E>()))
416 }
417 SqlStatementRoute::ShowIndexes { .. } => {
418 Ok(SqlDispatchResult::ShowIndexes(self.show_indexes::<E>()))
419 }
420 SqlStatementRoute::ShowColumns { .. } => {
421 Ok(SqlDispatchResult::ShowColumns(self.show_columns::<E>()))
422 }
423 SqlStatementRoute::ShowEntities => {
424 Ok(SqlDispatchResult::ShowEntities(self.show_entities()))
425 }
426 }
427 }
428
429 #[doc(hidden)]
436 pub fn execute_generated_query_surface_dispatch_for_authority(
437 &self,
438 parsed: &SqlParsedStatement,
439 authority: EntityAuthority,
440 ) -> Result<SqlDispatchResult, QueryError> {
441 match parsed.route() {
442 SqlStatementRoute::Query { .. } => {
443 if parsed_requires_dedicated_sql_aggregate_lane(parsed) {
444 return Err(QueryError::unsupported_query(
445 unsupported_sql_aggregate_lane_message(
446 SqlAggregateSurface::GeneratedQuerySurface,
447 ),
448 ));
449 }
450
451 if let Some(plan) =
452 computed_projection::computed_sql_projection_plan(&parsed.statement)?
453 {
454 return self
455 .execute_computed_sql_projection_dispatch_for_authority(plan, authority);
456 }
457
458 let lowered = parsed.lower_query_lane_for_entity(
459 authority.model().name(),
460 authority.model().primary_key.name,
461 )?;
462
463 Self::ensure_lowered_sql_query_grouping(
464 &lowered,
465 SqlGroupingSurface::GeneratedQuerySurface,
466 )?;
467
468 self.execute_lowered_sql_dispatch_query_for_authority(&lowered, authority)
469 }
470 SqlStatementRoute::Explain { .. } => {
471 if let Some((mode, plan)) =
472 computed_projection::computed_sql_projection_explain_plan(&parsed.statement)?
473 {
474 return self
475 .explain_computed_sql_projection_dispatch_for_authority(
476 mode, plan, authority,
477 )
478 .map(SqlDispatchResult::Explain);
479 }
480
481 let lowered = parsed.lower_query_lane_for_entity(
482 authority.model().name(),
483 authority.model().primary_key.name,
484 )?;
485 if let Some(explain) =
486 self.explain_lowered_sql_execution_for_authority(&lowered, authority)?
487 {
488 return Ok(SqlDispatchResult::Explain(explain));
489 }
490
491 self.explain_lowered_sql_for_authority(&lowered, authority)
492 .map(SqlDispatchResult::Explain)
493 }
494 SqlStatementRoute::Describe { .. }
495 | SqlStatementRoute::ShowIndexes { .. }
496 | SqlStatementRoute::ShowColumns { .. }
497 | SqlStatementRoute::ShowEntities => Err(QueryError::unsupported_query(
498 "generated SQL query surface requires query or EXPLAIN statement lanes",
499 )),
500 }
501 }
502
503 #[doc(hidden)]
509 #[must_use]
510 pub fn execute_generated_query_surface_sql(
511 &self,
512 sql: &str,
513 authorities: &[EntityAuthority],
514 ) -> GeneratedSqlDispatchAttempt {
515 let sql_trimmed = match trim_generated_query_sql_input(sql) {
518 Ok(sql_trimmed) => sql_trimmed,
519 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
520 };
521 let parsed = match self.parse_sql_statement(sql_trimmed) {
522 Ok(parsed) => parsed,
523 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
524 };
525
526 if matches!(parsed.route(), SqlStatementRoute::ShowEntities) {
529 return GeneratedSqlDispatchAttempt::new(
530 "",
531 None,
532 Ok(SqlDispatchResult::ShowEntities(generated_sql_entities(
533 authorities,
534 ))),
535 );
536 }
537 let authority = match authority_for_generated_sql_route(parsed.route(), authorities) {
538 Ok(authority) => authority,
539 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
540 };
541
542 let entity_name = authority.model().name();
546 let explain_order_field = parsed
547 .route()
548 .is_explain()
549 .then_some(authority.model().primary_key.name);
550 let result = match parsed.route() {
551 SqlStatementRoute::Query { .. } | SqlStatementRoute::Explain { .. } => {
552 self.execute_generated_query_surface_dispatch_for_authority(&parsed, authority)
553 }
554 SqlStatementRoute::Describe { .. } => Ok(SqlDispatchResult::Describe(
555 self.describe_entity_model(authority.model()),
556 )),
557 SqlStatementRoute::ShowIndexes { .. } => Ok(SqlDispatchResult::ShowIndexes(
558 self.show_indexes_for_store_model(authority.store_path(), authority.model()),
559 )),
560 SqlStatementRoute::ShowColumns { .. } => Ok(SqlDispatchResult::ShowColumns(
561 self.show_columns_for_model(authority.model()),
562 )),
563 SqlStatementRoute::ShowEntities => unreachable!(
564 "SHOW ENTITIES is handled before authority resolution for generated query dispatch"
565 ),
566 };
567
568 GeneratedSqlDispatchAttempt::new(entity_name, explain_order_field, result)
569 }
570}