1mod computed;
7mod lowered;
8
9use crate::{
10 db::{
11 DbSession, MissingRowPolicy, PersistedRow, Query, QueryError,
12 executor::{EntityAuthority, execute_sql_projection_rows_for_canister},
13 identifiers_tail_match,
14 query::intent::StructuralQuery,
15 session::sql::{
16 SqlDispatchResult, SqlParsedStatement, SqlStatementRoute,
17 aggregate::{
18 SqlAggregateSurface, parsed_requires_dedicated_sql_aggregate_lane,
19 unsupported_sql_aggregate_lane_message,
20 },
21 computed_projection,
22 projection::{
23 SqlProjectionPayload, projection_labels_from_entity_model,
24 projection_labels_from_projection_spec, sql_projection_rows_from_kernel_rows,
25 },
26 },
27 sql::lowering::{
28 LoweredSqlCommand, LoweredSqlQuery, bind_lowered_sql_query,
29 lower_sql_command_from_prepared_statement,
30 },
31 },
32 traits::{CanisterKind, EntityKind, EntityValue},
33};
34
35#[doc(hidden)]
44pub struct GeneratedSqlDispatchAttempt {
45 entity_name: &'static str,
46 explain_order_field: Option<&'static str>,
47 result: Result<SqlDispatchResult, QueryError>,
48}
49
50impl GeneratedSqlDispatchAttempt {
51 const fn new(
53 entity_name: &'static str,
54 explain_order_field: Option<&'static str>,
55 result: Result<SqlDispatchResult, QueryError>,
56 ) -> Self {
57 Self {
58 entity_name,
59 explain_order_field,
60 result,
61 }
62 }
63
64 #[must_use]
66 pub const fn entity_name(&self) -> &'static str {
67 self.entity_name
68 }
69
70 #[must_use]
72 pub const fn explain_order_field(&self) -> Option<&'static str> {
73 self.explain_order_field
74 }
75
76 pub fn into_result(self) -> Result<SqlDispatchResult, QueryError> {
78 self.result
79 }
80}
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub(in crate::db::session::sql) enum SqlGroupingSurface {
84 Scalar,
85 Dispatch,
86 GeneratedQuerySurface,
87 Grouped,
88}
89
90const fn unsupported_sql_grouping_message(surface: SqlGroupingSurface) -> &'static str {
91 match surface {
92 SqlGroupingSurface::Scalar => {
93 "execute_sql rejects grouped SELECT; use execute_sql_grouped(...)"
94 }
95 SqlGroupingSurface::Dispatch => {
96 "execute_sql_dispatch rejects grouped SELECT execution; use execute_sql_grouped(...)"
97 }
98 SqlGroupingSurface::GeneratedQuerySurface => {
99 "generated SQL query surface rejects grouped SELECT execution; use execute_sql_grouped(...)"
100 }
101 SqlGroupingSurface::Grouped => "execute_sql_grouped requires grouped SQL query intent",
102 }
103}
104
105fn trim_generated_query_sql_input(sql: &str) -> Result<&str, QueryError> {
108 let sql_trimmed = sql.trim();
109 if sql_trimmed.is_empty() {
110 return Err(QueryError::unsupported_query(
111 "query endpoint requires a non-empty SQL string",
112 ));
113 }
114
115 Ok(sql_trimmed)
116}
117
118fn generated_sql_entities(authorities: &[EntityAuthority]) -> Vec<String> {
121 let mut entities = Vec::with_capacity(authorities.len());
122
123 for authority in authorities {
124 entities.push(authority.model().name().to_string());
125 }
126
127 entities
128}
129
130fn authority_for_generated_sql_route(
132 route: &SqlStatementRoute,
133 authorities: &[EntityAuthority],
134) -> Result<EntityAuthority, QueryError> {
135 let sql_entity = route.entity();
136
137 for authority in authorities {
138 if identifiers_tail_match(sql_entity, authority.model().name()) {
139 return Ok(*authority);
140 }
141 }
142
143 Err(unsupported_generated_sql_entity_error(
144 sql_entity,
145 authorities,
146 ))
147}
148
149fn unsupported_generated_sql_entity_error(
152 entity_name: &str,
153 authorities: &[EntityAuthority],
154) -> QueryError {
155 let mut supported = String::new();
156
157 for (index, authority) in authorities.iter().enumerate() {
158 if index != 0 {
159 supported.push_str(", ");
160 }
161
162 supported.push_str(authority.model().name());
163 }
164
165 QueryError::unsupported_query(format!(
166 "query endpoint does not support entity '{entity_name}'; supported: {supported}"
167 ))
168}
169
170impl<C: CanisterKind> DbSession<C> {
171 fn execute_structural_sql_projection(
175 &self,
176 query: StructuralQuery,
177 authority: EntityAuthority,
178 ) -> Result<SqlProjectionPayload, QueryError> {
179 let plan = query.build_plan()?;
182 let projection = plan.projection_spec(authority.model());
183 let columns = projection_labels_from_projection_spec(&projection);
184
185 let projected = execute_sql_projection_rows_for_canister(
188 &self.db,
189 self.debug,
190 authority.model(),
191 projection,
192 authority,
193 plan,
194 )
195 .map_err(QueryError::execute)?;
196 let (rows, row_count) = projected.into_parts();
197
198 Ok(SqlProjectionPayload::new(columns, rows, row_count))
199 }
200
201 fn execute_typed_sql_delete<E>(&self, query: &Query<E>) -> Result<SqlDispatchResult, QueryError>
205 where
206 E: PersistedRow<Canister = C> + EntityValue,
207 {
208 let plan = query.plan()?.into_executable();
209 let deleted = self
210 .with_metrics(|| self.delete_executor::<E>().execute_sql_projection(plan))
211 .map_err(QueryError::execute)?;
212 let (rows, row_count) = deleted.into_parts();
213 let rows = sql_projection_rows_from_kernel_rows(rows);
214
215 Ok(SqlProjectionPayload::new(
216 projection_labels_from_entity_model(E::MODEL),
217 rows,
218 row_count,
219 )
220 .into_dispatch_result())
221 }
222
223 pub(in crate::db::session::sql) fn ensure_sql_query_grouping<E>(
226 query: &Query<E>,
227 surface: SqlGroupingSurface,
228 ) -> Result<(), QueryError>
229 where
230 E: EntityKind,
231 {
232 match (surface, query.has_grouping()) {
233 (
234 SqlGroupingSurface::Scalar
235 | SqlGroupingSurface::Dispatch
236 | SqlGroupingSurface::GeneratedQuerySurface,
237 false,
238 )
239 | (SqlGroupingSurface::Grouped, true) => Ok(()),
240 (
241 SqlGroupingSurface::Scalar
242 | SqlGroupingSurface::Dispatch
243 | SqlGroupingSurface::GeneratedQuerySurface,
244 true,
245 )
246 | (SqlGroupingSurface::Grouped, false) => Err(QueryError::unsupported_query(
247 unsupported_sql_grouping_message(surface),
248 )),
249 }
250 }
251
252 pub(in crate::db::session::sql) fn ensure_lowered_sql_query_grouping(
255 lowered: &LoweredSqlCommand,
256 surface: SqlGroupingSurface,
257 ) -> Result<(), QueryError> {
258 let Some(query) = lowered.query() else {
259 return Ok(());
260 };
261
262 match (surface, query.has_grouping()) {
263 (
264 SqlGroupingSurface::Scalar
265 | SqlGroupingSurface::Dispatch
266 | SqlGroupingSurface::GeneratedQuerySurface,
267 false,
268 )
269 | (SqlGroupingSurface::Grouped, true) => Ok(()),
270 (
271 SqlGroupingSurface::Scalar
272 | SqlGroupingSurface::Dispatch
273 | SqlGroupingSurface::GeneratedQuerySurface,
274 true,
275 )
276 | (SqlGroupingSurface::Grouped, false) => Err(QueryError::unsupported_query(
277 unsupported_sql_grouping_message(surface),
278 )),
279 }
280 }
281
282 pub fn execute_sql_dispatch<E>(&self, sql: &str) -> Result<SqlDispatchResult, QueryError>
284 where
285 E: PersistedRow<Canister = C> + EntityValue,
286 {
287 let parsed = self.parse_sql_statement(sql)?;
288
289 self.execute_sql_dispatch_parsed::<E>(&parsed)
290 }
291
292 pub fn execute_sql_dispatch_parsed<E>(
294 &self,
295 parsed: &SqlParsedStatement,
296 ) -> Result<SqlDispatchResult, QueryError>
297 where
298 E: PersistedRow<Canister = C> + EntityValue,
299 {
300 match parsed.route() {
301 SqlStatementRoute::Query { .. } => {
302 if parsed_requires_dedicated_sql_aggregate_lane(parsed) {
303 return Err(QueryError::unsupported_query(
304 unsupported_sql_aggregate_lane_message(
305 SqlAggregateSurface::ExecuteSqlDispatch,
306 ),
307 ));
308 }
309
310 if let Some(plan) =
311 computed_projection::computed_sql_projection_plan(&parsed.statement)?
312 {
313 return self.execute_computed_sql_projection_dispatch::<E>(plan);
314 }
315
316 let lowered = parsed
320 .lower_query_lane_for_entity(E::MODEL.name(), E::MODEL.primary_key.name)?;
321
322 Self::ensure_lowered_sql_query_grouping(&lowered, SqlGroupingSurface::Dispatch)?;
323
324 match lowered.query() {
328 Some(LoweredSqlQuery::Select(select)) => self
329 .execute_lowered_sql_dispatch_select_core(
330 select,
331 EntityAuthority::for_type::<E>(),
332 ),
333 Some(LoweredSqlQuery::Delete(delete)) => {
334 let typed_query = bind_lowered_sql_query::<E>(
335 LoweredSqlQuery::Delete(delete.clone()),
336 MissingRowPolicy::Ignore,
337 )
338 .map_err(QueryError::from_sql_lowering_error)?;
339
340 self.execute_typed_sql_delete(&typed_query)
341 }
342 None => Err(QueryError::unsupported_query(
343 "execute_sql_dispatch accepts SELECT or DELETE only",
344 )),
345 }
346 }
347 SqlStatementRoute::Explain { .. } => {
348 if let Some((mode, plan)) =
349 computed_projection::computed_sql_projection_explain_plan(&parsed.statement)?
350 {
351 return Self::explain_computed_sql_projection_dispatch::<E>(mode, plan)
352 .map(SqlDispatchResult::Explain);
353 }
354
355 let lowered = lower_sql_command_from_prepared_statement(
356 parsed.prepare(E::MODEL.name())?,
357 E::MODEL.primary_key.name,
358 )
359 .map_err(QueryError::from_sql_lowering_error)?;
360 if let Some(explain) = self.explain_lowered_sql_execution_for_authority(
361 &lowered,
362 EntityAuthority::for_type::<E>(),
363 )? {
364 return Ok(SqlDispatchResult::Explain(explain));
365 }
366
367 lowered
368 .explain_for_model(E::MODEL)
369 .map(SqlDispatchResult::Explain)
370 }
371 SqlStatementRoute::Describe { .. } => {
372 Ok(SqlDispatchResult::Describe(self.describe_entity::<E>()))
373 }
374 SqlStatementRoute::ShowIndexes { .. } => {
375 Ok(SqlDispatchResult::ShowIndexes(self.show_indexes::<E>()))
376 }
377 SqlStatementRoute::ShowColumns { .. } => {
378 Ok(SqlDispatchResult::ShowColumns(self.show_columns::<E>()))
379 }
380 SqlStatementRoute::ShowEntities => {
381 Ok(SqlDispatchResult::ShowEntities(self.show_entities()))
382 }
383 }
384 }
385
386 #[doc(hidden)]
393 pub fn execute_generated_query_surface_dispatch_for_authority(
394 &self,
395 parsed: &SqlParsedStatement,
396 authority: EntityAuthority,
397 ) -> Result<SqlDispatchResult, QueryError> {
398 match parsed.route() {
399 SqlStatementRoute::Query { .. } => {
400 if parsed_requires_dedicated_sql_aggregate_lane(parsed) {
401 return Err(QueryError::unsupported_query(
402 unsupported_sql_aggregate_lane_message(
403 SqlAggregateSurface::GeneratedQuerySurface,
404 ),
405 ));
406 }
407
408 if let Some(plan) =
409 computed_projection::computed_sql_projection_plan(&parsed.statement)?
410 {
411 return self
412 .execute_computed_sql_projection_dispatch_for_authority(plan, authority);
413 }
414
415 let lowered = parsed.lower_query_lane_for_entity(
416 authority.model().name(),
417 authority.model().primary_key.name,
418 )?;
419
420 Self::ensure_lowered_sql_query_grouping(
421 &lowered,
422 SqlGroupingSurface::GeneratedQuerySurface,
423 )?;
424
425 self.execute_lowered_sql_dispatch_query_for_authority(&lowered, authority)
426 }
427 SqlStatementRoute::Explain { .. } => {
428 if let Some((mode, plan)) =
429 computed_projection::computed_sql_projection_explain_plan(&parsed.statement)?
430 {
431 return Self::explain_computed_sql_projection_dispatch_for_authority(
432 mode, plan, authority,
433 )
434 .map(SqlDispatchResult::Explain);
435 }
436
437 let lowered = parsed.lower_query_lane_for_entity(
438 authority.model().name(),
439 authority.model().primary_key.name,
440 )?;
441 if let Some(explain) =
442 self.explain_lowered_sql_execution_for_authority(&lowered, authority)?
443 {
444 return Ok(SqlDispatchResult::Explain(explain));
445 }
446
447 lowered
448 .explain_for_model(authority.model())
449 .map(SqlDispatchResult::Explain)
450 }
451 SqlStatementRoute::Describe { .. }
452 | SqlStatementRoute::ShowIndexes { .. }
453 | SqlStatementRoute::ShowColumns { .. }
454 | SqlStatementRoute::ShowEntities => Err(QueryError::unsupported_query(
455 "generated SQL query surface requires query or EXPLAIN statement lanes",
456 )),
457 }
458 }
459
460 #[doc(hidden)]
466 #[must_use]
467 pub fn execute_generated_query_surface_sql(
468 &self,
469 sql: &str,
470 authorities: &[EntityAuthority],
471 ) -> GeneratedSqlDispatchAttempt {
472 let sql_trimmed = match trim_generated_query_sql_input(sql) {
475 Ok(sql_trimmed) => sql_trimmed,
476 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
477 };
478 let parsed = match self.parse_sql_statement(sql_trimmed) {
479 Ok(parsed) => parsed,
480 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
481 };
482
483 if matches!(parsed.route(), SqlStatementRoute::ShowEntities) {
486 return GeneratedSqlDispatchAttempt::new(
487 "",
488 None,
489 Ok(SqlDispatchResult::ShowEntities(generated_sql_entities(
490 authorities,
491 ))),
492 );
493 }
494 let authority = match authority_for_generated_sql_route(parsed.route(), authorities) {
495 Ok(authority) => authority,
496 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
497 };
498
499 let entity_name = authority.model().name();
503 let explain_order_field = parsed
504 .route()
505 .is_explain()
506 .then_some(authority.model().primary_key.name);
507 let result = match parsed.route() {
508 SqlStatementRoute::Query { .. } | SqlStatementRoute::Explain { .. } => {
509 self.execute_generated_query_surface_dispatch_for_authority(&parsed, authority)
510 }
511 SqlStatementRoute::Describe { .. } => Ok(SqlDispatchResult::Describe(
512 self.describe_entity_model(authority.model()),
513 )),
514 SqlStatementRoute::ShowIndexes { .. } => Ok(SqlDispatchResult::ShowIndexes(
515 self.show_indexes_for_model(authority.model()),
516 )),
517 SqlStatementRoute::ShowColumns { .. } => Ok(SqlDispatchResult::ShowColumns(
518 self.show_columns_for_model(authority.model()),
519 )),
520 SqlStatementRoute::ShowEntities => unreachable!(
521 "SHOW ENTITIES is handled before authority resolution for generated query dispatch"
522 ),
523 };
524
525 GeneratedSqlDispatchAttempt::new(entity_name, explain_order_field, result)
526 }
527}