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