icydb_core/db/sql/lowering/
mod.rs1mod aggregate;
7mod expr;
8mod normalize;
9mod predicate;
10mod prepare;
11mod select;
12
13#[cfg(test)]
18mod tests;
19
20use crate::db::{
21 query::intent::QueryError,
22 sql::parser::{SqlExplainMode, SqlStatement},
23};
24#[cfg(test)]
25use crate::{
26 db::{predicate::MissingRowPolicy, query::intent::Query},
27 traits::EntityKind,
28};
29use thiserror::Error as ThisError;
30
31pub(in crate::db::sql::lowering) use aggregate::LoweredSqlGlobalAggregateCommand;
32pub(in crate::db) use aggregate::compile_sql_global_aggregate_command_core_from_prepared;
33pub(in crate::db) use aggregate::is_sql_global_aggregate_statement;
34#[cfg(test)]
35pub(crate) use aggregate::{
36 PreparedSqlScalarAggregateDescriptorShape, PreparedSqlScalarAggregateDomain,
37 PreparedSqlScalarAggregateEmptySetBehavior, PreparedSqlScalarAggregateOrderingRequirement,
38 PreparedSqlScalarAggregateRowSource, SqlGlobalAggregateCommand,
39 TypedSqlGlobalAggregateTerminal, compile_sql_global_aggregate_command,
40};
41pub(crate) use aggregate::{
42 PreparedSqlScalarAggregateRuntimeDescriptor, PreparedSqlScalarAggregateStrategy,
43 SqlGlobalAggregateCommandCore, bind_lowered_sql_explain_global_aggregate_structural,
44};
45pub(in crate::db) use predicate::lower_sql_where_expr;
46pub(crate) use prepare::{lower_sql_command_from_prepared_statement, prepare_sql_statement};
47pub(in crate::db::sql::lowering) use select::apply_lowered_base_query_shape;
48#[cfg(test)]
49pub(in crate::db) use select::apply_lowered_select_shape;
50pub(crate) use select::{LoweredBaseQueryShape, LoweredSelectShape};
51pub(in crate::db) use select::{
52 bind_lowered_sql_query, bind_lowered_sql_query_structural,
53 bind_lowered_sql_select_query_structural, canonicalize_sql_predicate_for_model,
54 canonicalize_strict_sql_literal_for_kind,
55};
56
57#[derive(Clone, Debug)]
66pub struct LoweredSqlCommand(pub(in crate::db::sql::lowering) LoweredSqlCommandInner);
67
68#[derive(Clone, Debug)]
69#[expect(
70 clippy::large_enum_variant,
71 reason = "global aggregate lowering keeps one owned command payload on the generic-free SQL seam"
72)]
73pub(in crate::db::sql::lowering) enum LoweredSqlCommandInner {
74 Query(LoweredSqlQuery),
75 Explain {
76 mode: SqlExplainMode,
77 query: LoweredSqlQuery,
78 },
79 ExplainGlobalAggregate {
80 mode: SqlExplainMode,
81 command: LoweredSqlGlobalAggregateCommand,
82 },
83 DescribeEntity,
84 ShowIndexesEntity,
85 ShowColumnsEntity,
86 ShowEntities,
87}
88
89#[cfg(test)]
97#[derive(Debug)]
98#[expect(
99 clippy::large_enum_variant,
100 reason = "typed SQL tests keep one owned global aggregate command to validate binding without adding box indirection"
101)]
102pub(crate) enum SqlCommand<E: EntityKind> {
103 Query(Query<E>),
104 Explain {
105 mode: SqlExplainMode,
106 query: Query<E>,
107 },
108 ExplainGlobalAggregate {
109 mode: SqlExplainMode,
110 command: SqlGlobalAggregateCommand<E>,
111 },
112 DescribeEntity,
113 ShowIndexesEntity,
114 ShowColumnsEntity,
115 ShowEntities,
116}
117
118impl LoweredSqlCommand {
119 #[must_use]
120 #[cfg_attr(not(any(test, feature = "diagnostics")), allow(dead_code))]
121 pub(in crate::db) const fn query(&self) -> Option<&LoweredSqlQuery> {
122 match &self.0 {
123 LoweredSqlCommandInner::Query(query) => Some(query),
124 LoweredSqlCommandInner::Explain { .. }
125 | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
126 | LoweredSqlCommandInner::DescribeEntity
127 | LoweredSqlCommandInner::ShowIndexesEntity
128 | LoweredSqlCommandInner::ShowColumnsEntity
129 | LoweredSqlCommandInner::ShowEntities => None,
130 }
131 }
132
133 #[must_use]
134 pub(in crate::db) fn into_query(self) -> Option<LoweredSqlQuery> {
135 match self.0 {
136 LoweredSqlCommandInner::Query(query) => Some(query),
137 LoweredSqlCommandInner::Explain { .. }
138 | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
139 | LoweredSqlCommandInner::DescribeEntity
140 | LoweredSqlCommandInner::ShowIndexesEntity
141 | LoweredSqlCommandInner::ShowColumnsEntity
142 | LoweredSqlCommandInner::ShowEntities => None,
143 }
144 }
145
146 #[must_use]
147 pub(in crate::db) const fn explain_query(&self) -> Option<(SqlExplainMode, &LoweredSqlQuery)> {
148 match &self.0 {
149 LoweredSqlCommandInner::Explain { mode, query } => Some((*mode, query)),
150 LoweredSqlCommandInner::Query(_)
151 | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
152 | LoweredSqlCommandInner::DescribeEntity
153 | LoweredSqlCommandInner::ShowIndexesEntity
154 | LoweredSqlCommandInner::ShowColumnsEntity
155 | LoweredSqlCommandInner::ShowEntities => None,
156 }
157 }
158}
159
160#[derive(Clone, Debug)]
167pub(crate) enum LoweredSqlQuery {
168 Select(LoweredSelectShape),
169 Delete(LoweredBaseQueryShape),
170}
171
172#[derive(Debug, ThisError)]
178pub(crate) enum SqlLoweringError {
179 #[error("{0}")]
180 Parse(#[from] crate::db::sql::parser::SqlParseError),
181
182 #[error("{0}")]
183 Query(Box<QueryError>),
184
185 #[error("SQL entity '{sql_entity}' does not match requested entity type '{expected_entity}'")]
186 EntityMismatch {
187 sql_entity: String,
188 expected_entity: &'static str,
189 },
190
191 #[error(
192 "unsupported SQL SELECT projection; supported forms are SELECT *, field lists, global aggregate terminal lists, or grouped aggregate shapes"
193 )]
194 UnsupportedSelectProjection,
195
196 #[error("unsupported SQL SELECT DISTINCT")]
197 UnsupportedSelectDistinct,
198
199 #[error(
200 "unsupported global aggregate SQL projection; supported forms are aggregate projections such as COUNT(*), SUM(field), AVG(expr), or scalar wrappers over aggregate results"
201 )]
202 UnsupportedGlobalAggregateProjection,
203
204 #[error("global aggregate SQL does not support GROUP BY")]
205 GlobalAggregateDoesNotSupportGroupBy,
206
207 #[error("unsupported SQL GROUP BY projection shape")]
208 UnsupportedSelectGroupBy,
209
210 #[error("grouped SELECT requires an explicit projection list")]
211 GroupedProjectionRequiresExplicitList,
212
213 #[error("grouped SELECT projection must include at least one aggregate expression")]
214 GroupedProjectionRequiresAggregate,
215
216 #[error(
217 "grouped projection expression at index={index} references fields outside GROUP BY keys"
218 )]
219 GroupedProjectionReferencesNonGroupField { index: usize },
220
221 #[error(
222 "grouped projection expression at index={index} appears after aggregate expressions started"
223 )]
224 GroupedProjectionScalarAfterAggregate { index: usize },
225
226 #[error("HAVING requires GROUP BY")]
227 HavingRequiresGroupBy,
228
229 #[error("unsupported SQL HAVING shape")]
230 UnsupportedSelectHaving,
231
232 #[error("aggregate input expressions are not executable in this release")]
233 UnsupportedAggregateInputExpressions,
234
235 #[error("unsupported SQL WHERE expression shape")]
236 UnsupportedWhereExpression,
237
238 #[error("unknown field '{field}'")]
239 UnknownField { field: String },
240
241 #[error("ORDER BY alias '{alias}' does not resolve to a supported order target")]
242 UnsupportedOrderByAlias { alias: String },
243
244 #[error("query-lane lowering reached a non query-compatible statement")]
245 UnexpectedQueryLaneStatement,
246}
247
248impl SqlLoweringError {
249 fn entity_mismatch(sql_entity: impl Into<String>, expected_entity: &'static str) -> Self {
251 Self::EntityMismatch {
252 sql_entity: sql_entity.into(),
253 expected_entity,
254 }
255 }
256
257 const fn unsupported_select_projection() -> Self {
259 Self::UnsupportedSelectProjection
260 }
261
262 pub(crate) const fn unexpected_query_lane_statement() -> Self {
264 Self::UnexpectedQueryLaneStatement
265 }
266
267 const fn unsupported_select_distinct() -> Self {
269 Self::UnsupportedSelectDistinct
270 }
271
272 const fn unsupported_global_aggregate_projection() -> Self {
274 Self::UnsupportedGlobalAggregateProjection
275 }
276
277 pub(crate) const fn unsupported_where_expression() -> Self {
279 Self::UnsupportedWhereExpression
280 }
281
282 const fn global_aggregate_does_not_support_group_by() -> Self {
284 Self::GlobalAggregateDoesNotSupportGroupBy
285 }
286
287 const fn unsupported_select_group_by() -> Self {
289 Self::UnsupportedSelectGroupBy
290 }
291
292 const fn grouped_projection_requires_explicit_list() -> Self {
294 Self::GroupedProjectionRequiresExplicitList
295 }
296
297 const fn grouped_projection_requires_aggregate() -> Self {
299 Self::GroupedProjectionRequiresAggregate
300 }
301
302 const fn grouped_projection_references_non_group_field(index: usize) -> Self {
304 Self::GroupedProjectionReferencesNonGroupField { index }
305 }
306
307 const fn grouped_projection_scalar_after_aggregate(index: usize) -> Self {
309 Self::GroupedProjectionScalarAfterAggregate { index }
310 }
311
312 const fn having_requires_group_by() -> Self {
314 Self::HavingRequiresGroupBy
315 }
316
317 const fn unsupported_select_having() -> Self {
319 Self::UnsupportedSelectHaving
320 }
321
322 const fn unsupported_aggregate_input_expressions() -> Self {
324 Self::UnsupportedAggregateInputExpressions
325 }
326
327 fn unknown_field(field: impl Into<String>) -> Self {
329 Self::UnknownField {
330 field: field.into(),
331 }
332 }
333
334 fn unsupported_order_by_alias(alias: impl Into<String>) -> Self {
336 Self::UnsupportedOrderByAlias {
337 alias: alias.into(),
338 }
339 }
340}
341
342impl From<QueryError> for SqlLoweringError {
343 fn from(value: QueryError) -> Self {
344 Self::Query(Box::new(value))
345 }
346}
347
348#[derive(Clone, Debug)]
358pub(crate) struct PreparedSqlStatement {
359 pub(in crate::db::sql::lowering) statement: SqlStatement,
360}
361
362impl PreparedSqlStatement {
363 #[must_use]
365 pub(in crate::db) fn into_statement(self) -> SqlStatement {
366 self.statement
367 }
368}
369
370#[derive(Clone, Copy, Debug, Eq, PartialEq)]
371pub(crate) enum LoweredSqlLaneKind {
372 Query,
373 Explain,
374 Describe,
375 ShowIndexes,
376 ShowColumns,
377 ShowEntities,
378}
379
380#[cfg(test)]
382pub(crate) fn compile_sql_command<E: EntityKind>(
383 sql: &str,
384 consistency: MissingRowPolicy,
385) -> Result<SqlCommand<E>, SqlLoweringError> {
386 let statement = crate::db::sql::parser::parse_sql(sql)?;
387 let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
388 let lowered = lower_sql_command_from_prepared_statement(prepared, E::MODEL)?;
389
390 match lowered.0 {
393 LoweredSqlCommandInner::Query(query) => Ok(SqlCommand::Query(bind_lowered_sql_query::<E>(
394 query,
395 consistency,
396 )?)),
397 LoweredSqlCommandInner::Explain { mode, query } => Ok(SqlCommand::Explain {
398 mode,
399 query: bind_lowered_sql_query::<E>(query, consistency)?,
400 }),
401 LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } => {
402 Ok(SqlCommand::ExplainGlobalAggregate {
403 mode,
404 command: aggregate::bind_lowered_sql_global_aggregate_command::<E>(
405 command,
406 consistency,
407 )?,
408 })
409 }
410 LoweredSqlCommandInner::DescribeEntity => Ok(SqlCommand::DescribeEntity),
411 LoweredSqlCommandInner::ShowIndexesEntity => Ok(SqlCommand::ShowIndexesEntity),
412 LoweredSqlCommandInner::ShowColumnsEntity => Ok(SqlCommand::ShowColumnsEntity),
413 LoweredSqlCommandInner::ShowEntities => Ok(SqlCommand::ShowEntities),
414 }
415}
416
417pub(crate) const fn lowered_sql_command_lane(command: &LoweredSqlCommand) -> LoweredSqlLaneKind {
418 match command.0 {
419 LoweredSqlCommandInner::Query(_) => LoweredSqlLaneKind::Query,
420 LoweredSqlCommandInner::Explain { .. }
421 | LoweredSqlCommandInner::ExplainGlobalAggregate { .. } => LoweredSqlLaneKind::Explain,
422 LoweredSqlCommandInner::DescribeEntity => LoweredSqlLaneKind::Describe,
423 LoweredSqlCommandInner::ShowIndexesEntity => LoweredSqlLaneKind::ShowIndexes,
424 LoweredSqlCommandInner::ShowColumnsEntity => LoweredSqlLaneKind::ShowColumns,
425 LoweredSqlCommandInner::ShowEntities => LoweredSqlLaneKind::ShowEntities,
426 }
427}