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_structural_query, sql_projection_rows_from_kernel_rows,
25 },
26 surface::{SqlSurface, session_sql_lane, unsupported_sql_lane_message},
27 },
28 sql::lowering::{
29 LoweredSqlCommand, LoweredSqlQuery, bind_lowered_sql_query,
30 lower_sql_command_from_prepared_statement,
31 },
32 },
33 traits::{CanisterKind, EntityKind, EntityValue},
34};
35
36#[doc(hidden)]
45pub struct GeneratedSqlDispatchAttempt {
46 entity_name: &'static str,
47 explain_order_field: Option<&'static str>,
48 result: Result<SqlDispatchResult, QueryError>,
49}
50
51impl GeneratedSqlDispatchAttempt {
52 const fn new(
54 entity_name: &'static str,
55 explain_order_field: Option<&'static str>,
56 result: Result<SqlDispatchResult, QueryError>,
57 ) -> Self {
58 Self {
59 entity_name,
60 explain_order_field,
61 result,
62 }
63 }
64
65 #[must_use]
67 pub const fn entity_name(&self) -> &'static str {
68 self.entity_name
69 }
70
71 #[must_use]
73 pub const fn explain_order_field(&self) -> Option<&'static str> {
74 self.explain_order_field
75 }
76
77 pub fn into_result(self) -> Result<SqlDispatchResult, QueryError> {
79 self.result
80 }
81}
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub(in crate::db::session::sql) enum SqlGroupingSurface {
85 Scalar,
86 Dispatch,
87 GeneratedQuerySurface,
88 Grouped,
89}
90
91const fn unsupported_sql_grouping_message(surface: SqlGroupingSurface) -> &'static str {
92 match surface {
93 SqlGroupingSurface::Scalar => {
94 "execute_sql rejects grouped SELECT; use execute_sql_grouped(...)"
95 }
96 SqlGroupingSurface::Dispatch => {
97 "execute_sql_dispatch rejects grouped SELECT execution; use execute_sql_grouped(...)"
98 }
99 SqlGroupingSurface::GeneratedQuerySurface => {
100 "generated SQL query surface rejects grouped SELECT execution; use execute_sql_grouped(...)"
101 }
102 SqlGroupingSurface::Grouped => "execute_sql_grouped requires grouped SQL query intent",
103 }
104}
105
106fn trim_generated_query_sql_input(sql: &str) -> Result<&str, QueryError> {
109 let sql_trimmed = sql.trim();
110 if sql_trimmed.is_empty() {
111 return Err(QueryError::unsupported_query(
112 "query endpoint requires a non-empty SQL string",
113 ));
114 }
115
116 Ok(sql_trimmed)
117}
118
119fn generated_sql_entities(authorities: &[EntityAuthority]) -> Vec<String> {
122 let mut entities = Vec::with_capacity(authorities.len());
123
124 for authority in authorities {
125 entities.push(authority.model().name().to_string());
126 }
127
128 entities
129}
130
131fn authority_for_generated_sql_route(
133 route: &SqlStatementRoute,
134 authorities: &[EntityAuthority],
135) -> Result<EntityAuthority, QueryError> {
136 let sql_entity = route.entity();
137
138 for authority in authorities {
139 if identifiers_tail_match(sql_entity, authority.model().name()) {
140 return Ok(*authority);
141 }
142 }
143
144 Err(unsupported_generated_sql_entity_error(
145 sql_entity,
146 authorities,
147 ))
148}
149
150fn unsupported_generated_sql_entity_error(
153 entity_name: &str,
154 authorities: &[EntityAuthority],
155) -> QueryError {
156 let mut supported = String::new();
157
158 for (index, authority) in authorities.iter().enumerate() {
159 if index != 0 {
160 supported.push_str(", ");
161 }
162
163 supported.push_str(authority.model().name());
164 }
165
166 QueryError::unsupported_query(format!(
167 "query endpoint does not support entity '{entity_name}'; supported: {supported}"
168 ))
169}
170
171impl<C: CanisterKind> DbSession<C> {
172 pub(in crate::db::session::sql) fn bind_sql_query_lane_from_parsed<E>(
175 parsed: &SqlParsedStatement,
176 ) -> Result<(LoweredSqlQuery, Query<E>), QueryError>
177 where
178 E: EntityKind<Canister = C>,
179 {
180 if computed_projection::computed_sql_projection_plan(&parsed.statement)?.is_some() {
184 return Err(QueryError::unsupported_query(
185 "query_from_sql does not accept computed text projection; use execute_sql_dispatch(...)",
186 ));
187 }
188
189 let lowered =
190 parsed.lower_query_lane_for_entity(E::MODEL.name(), E::MODEL.primary_key.name)?;
191 let lane = session_sql_lane(&lowered);
192 let Some(query) = lowered.query().cloned() else {
193 return Err(QueryError::unsupported_query(unsupported_sql_lane_message(
194 SqlSurface::QueryFrom,
195 lane,
196 )));
197 };
198 let typed = bind_lowered_sql_query::<E>(query.clone(), MissingRowPolicy::Ignore)
199 .map_err(QueryError::from_sql_lowering_error)?;
200
201 Ok((query, typed))
202 }
203
204 fn execute_structural_sql_projection(
208 &self,
209 query: StructuralQuery,
210 authority: EntityAuthority,
211 ) -> Result<SqlProjectionPayload, QueryError> {
212 let columns = projection_labels_from_structural_query(&query)?;
213 let projected = execute_sql_projection_rows_for_canister(
214 &self.db,
215 self.debug,
216 authority,
217 query.build_plan()?,
218 )
219 .map_err(QueryError::execute)?;
220 let (rows, row_count) = projected.into_parts();
221
222 Ok(SqlProjectionPayload::new(columns, rows, row_count))
223 }
224
225 fn execute_typed_sql_delete<E>(&self, query: &Query<E>) -> Result<SqlDispatchResult, QueryError>
229 where
230 E: PersistedRow<Canister = C> + EntityValue,
231 {
232 let plan = query.plan()?.into_executable();
233 let deleted = self
234 .with_metrics(|| self.delete_executor::<E>().execute_sql_projection(plan))
235 .map_err(QueryError::execute)?;
236 let (rows, row_count) = deleted.into_parts();
237 let rows = sql_projection_rows_from_kernel_rows(rows);
238
239 Ok(SqlProjectionPayload::new(
240 projection_labels_from_entity_model(E::MODEL),
241 rows,
242 row_count,
243 )
244 .into_dispatch_result())
245 }
246
247 pub(in crate::db::session::sql) fn ensure_sql_query_grouping<E>(
250 query: &Query<E>,
251 surface: SqlGroupingSurface,
252 ) -> Result<(), QueryError>
253 where
254 E: EntityKind,
255 {
256 match (surface, query.has_grouping()) {
257 (
258 SqlGroupingSurface::Scalar
259 | SqlGroupingSurface::Dispatch
260 | SqlGroupingSurface::GeneratedQuerySurface,
261 false,
262 )
263 | (SqlGroupingSurface::Grouped, true) => Ok(()),
264 (
265 SqlGroupingSurface::Scalar
266 | SqlGroupingSurface::Dispatch
267 | SqlGroupingSurface::GeneratedQuerySurface,
268 true,
269 )
270 | (SqlGroupingSurface::Grouped, false) => Err(QueryError::unsupported_query(
271 unsupported_sql_grouping_message(surface),
272 )),
273 }
274 }
275
276 pub(in crate::db::session::sql) fn ensure_lowered_sql_query_grouping(
279 lowered: &LoweredSqlCommand,
280 surface: SqlGroupingSurface,
281 ) -> Result<(), QueryError> {
282 let Some(query) = lowered.query() else {
283 return Ok(());
284 };
285
286 match (surface, query.has_grouping()) {
287 (
288 SqlGroupingSurface::Scalar
289 | SqlGroupingSurface::Dispatch
290 | SqlGroupingSurface::GeneratedQuerySurface,
291 false,
292 )
293 | (SqlGroupingSurface::Grouped, true) => Ok(()),
294 (
295 SqlGroupingSurface::Scalar
296 | SqlGroupingSurface::Dispatch
297 | SqlGroupingSurface::GeneratedQuerySurface,
298 true,
299 )
300 | (SqlGroupingSurface::Grouped, false) => Err(QueryError::unsupported_query(
301 unsupported_sql_grouping_message(surface),
302 )),
303 }
304 }
305
306 pub fn execute_sql_dispatch<E>(&self, sql: &str) -> Result<SqlDispatchResult, QueryError>
308 where
309 E: PersistedRow<Canister = C> + EntityValue,
310 {
311 let parsed = self.parse_sql_statement(sql)?;
312
313 self.execute_sql_dispatch_parsed::<E>(&parsed)
314 }
315
316 pub fn execute_sql_dispatch_parsed<E>(
318 &self,
319 parsed: &SqlParsedStatement,
320 ) -> Result<SqlDispatchResult, QueryError>
321 where
322 E: PersistedRow<Canister = C> + EntityValue,
323 {
324 match parsed.route() {
325 SqlStatementRoute::Query { .. } => {
326 if parsed_requires_dedicated_sql_aggregate_lane(parsed) {
327 return Err(QueryError::unsupported_query(
328 unsupported_sql_aggregate_lane_message(
329 SqlAggregateSurface::ExecuteSqlDispatch,
330 ),
331 ));
332 }
333
334 if let Some(plan) =
335 computed_projection::computed_sql_projection_plan(&parsed.statement)?
336 {
337 return self.execute_computed_sql_projection_dispatch::<E>(plan);
338 }
339
340 let (query, typed_query) = Self::bind_sql_query_lane_from_parsed::<E>(parsed)?;
341
342 Self::ensure_sql_query_grouping(&typed_query, SqlGroupingSurface::Dispatch)?;
343
344 match query {
345 LoweredSqlQuery::Select(select) => self
346 .execute_lowered_sql_dispatch_select_core(
347 &select,
348 EntityAuthority::for_type::<E>(),
349 ),
350 LoweredSqlQuery::Delete(_) => self.execute_typed_sql_delete(&typed_query),
351 }
352 }
353 SqlStatementRoute::Explain { .. } => {
354 if let Some((mode, plan)) =
355 computed_projection::computed_sql_projection_explain_plan(&parsed.statement)?
356 {
357 return Self::explain_computed_sql_projection_dispatch::<E>(mode, plan)
358 .map(SqlDispatchResult::Explain);
359 }
360
361 let lowered = lower_sql_command_from_prepared_statement(
362 parsed.prepare(E::MODEL.name())?,
363 E::MODEL.primary_key.name,
364 )
365 .map_err(QueryError::from_sql_lowering_error)?;
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_generated_query_surface_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_generated_query_surface_for_entity(
438 authority.model().name(),
439 authority.model().primary_key.name,
440 )?;
441
442 lowered
443 .explain_for_model(authority.model())
444 .map(SqlDispatchResult::Explain)
445 }
446 SqlStatementRoute::Describe { .. }
447 | SqlStatementRoute::ShowIndexes { .. }
448 | SqlStatementRoute::ShowColumns { .. }
449 | SqlStatementRoute::ShowEntities => Err(QueryError::unsupported_query(
450 "generated SQL query surface requires query or EXPLAIN statement lanes",
451 )),
452 }
453 }
454
455 #[doc(hidden)]
461 #[must_use]
462 pub fn execute_generated_query_surface_sql(
463 &self,
464 sql: &str,
465 authorities: &[EntityAuthority],
466 ) -> GeneratedSqlDispatchAttempt {
467 let sql_trimmed = match trim_generated_query_sql_input(sql) {
470 Ok(sql_trimmed) => sql_trimmed,
471 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
472 };
473 let parsed = match self.parse_sql_statement(sql_trimmed) {
474 Ok(parsed) => parsed,
475 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
476 };
477
478 if matches!(parsed.route(), SqlStatementRoute::ShowEntities) {
481 return GeneratedSqlDispatchAttempt::new(
482 "",
483 None,
484 Ok(SqlDispatchResult::ShowEntities(generated_sql_entities(
485 authorities,
486 ))),
487 );
488 }
489 let authority = match authority_for_generated_sql_route(parsed.route(), authorities) {
490 Ok(authority) => authority,
491 Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
492 };
493
494 let entity_name = authority.model().name();
498 let explain_order_field = parsed
499 .route()
500 .is_explain()
501 .then_some(authority.model().primary_key.name);
502 let result = match parsed.route() {
503 SqlStatementRoute::Query { .. } | SqlStatementRoute::Explain { .. } => {
504 self.execute_generated_query_surface_dispatch_for_authority(&parsed, authority)
505 }
506 SqlStatementRoute::Describe { .. } => Ok(SqlDispatchResult::Describe(
507 self.describe_entity_model(authority.model()),
508 )),
509 SqlStatementRoute::ShowIndexes { .. } => Ok(SqlDispatchResult::ShowIndexes(
510 self.show_indexes_for_model(authority.model()),
511 )),
512 SqlStatementRoute::ShowColumns { .. } => Ok(SqlDispatchResult::ShowColumns(
513 self.show_columns_for_model(authority.model()),
514 )),
515 SqlStatementRoute::ShowEntities => unreachable!(
516 "SHOW ENTITIES is handled before authority resolution for generated query dispatch"
517 ),
518 };
519
520 GeneratedSqlDispatchAttempt::new(entity_name, explain_order_field, result)
521 }
522}