Skip to main content

radixdb_orm/
sql.rs

1use std::fmt::Write;
2
3use sha2::{Digest, Sha256};
4
5use crate::*;
6
7#[derive(Debug, thiserror::Error)]
8pub enum RenderError {
9    #[error("invalid SQL identifier: {0}")]
10    InvalidIdentifier(String),
11    #[error("invalid ORM operation: {0}")]
12    InvalidOperation(String),
13    #[error("unsupported ORM operation: {0}")]
14    Unsupported(String),
15}
16
17#[derive(Debug, Clone, PartialEq)]
18pub struct CompiledStatement {
19    pub sql: String,
20    pub parameters: Vec<TypedValue>,
21    pub expected_result_shape: Vec<ResultColumnDescriptor>,
22    pub shape_fingerprint: String,
23}
24
25impl IrDocument {
26    pub fn to_sql(&self) -> Result<CompiledStatement, RenderError> {
27        self.validate()
28            .map_err(|error| RenderError::InvalidOperation(error.to_string()))?;
29        SqlRenderer::compile(&self.payload)
30    }
31}
32
33pub struct SqlRenderer {
34    sql: String,
35    parameters: Vec<TypedValue>,
36}
37
38impl SqlRenderer {
39    pub fn compile(operation: &Operation) -> Result<CompiledStatement, RenderError> {
40        let mut renderer = Self {
41            sql: String::new(),
42            parameters: Vec::new(),
43        };
44        renderer.render_operation(operation)?;
45        let expected_result_shape = match operation {
46            Operation::Select { query } => query.expected_result_shape.clone(),
47            _ => Vec::new(),
48        };
49        let shape_fingerprint =
50            shape_fingerprint(&renderer.sql, &renderer.parameters, &expected_result_shape);
51        Ok(CompiledStatement {
52            sql: renderer.sql,
53            parameters: renderer.parameters,
54            expected_result_shape,
55            shape_fingerprint,
56        })
57    }
58
59    fn render_operation(&mut self, operation: &Operation) -> Result<(), RenderError> {
60        match operation {
61            Operation::Catalog { operation } => self.render_catalog(operation),
62            Operation::Ddl { operation } => self.render_ddl(operation),
63            Operation::Select { query } => self.render_select(query),
64            Operation::Insert { statement } => self.render_insert(statement),
65            Operation::Upsert { statement } => self.render_upsert(statement),
66            Operation::Update { statement } => self.render_update(statement),
67            Operation::Delete { statement } => self.render_delete(statement),
68            Operation::Explain { statement } => {
69                self.sql.push_str("EXPLAIN ");
70                if statement.analyze {
71                    self.sql.push_str("ANALYZE ");
72                }
73                match statement.operation.as_ref() {
74                    Operation::Select { .. }
75                    | Operation::Insert { .. }
76                    | Operation::Upsert { .. }
77                    | Operation::Update { .. }
78                    | Operation::Delete { .. } => self.render_operation(&statement.operation),
79                    _ => Err(RenderError::InvalidOperation(
80                        "EXPLAIN accepts query or DML operations only".to_string(),
81                    )),
82                }
83            }
84            Operation::Transaction { statement } => self.render_transaction(statement),
85        }
86    }
87
88    fn render_catalog(&mut self, operation: &CatalogOperation) -> Result<(), RenderError> {
89        match operation {
90            CatalogOperation::ListTables => self.sql.push_str("SHOW TABLES"),
91            CatalogOperation::DescribeTable { table } => {
92                self.sql.push_str("DESCRIBE TABLE ");
93                self.identifier(table)?;
94                self.sql.push_str(" FORMAT JSON");
95            }
96            CatalogOperation::DescribeDatabase => {
97                self.sql.push_str("DESCRIBE DATABASE FORMAT JSON")
98            }
99            CatalogOperation::ShowIndexes { table } => {
100                self.sql.push_str("SHOW INDEXES FROM ");
101                self.identifier(table)?;
102            }
103        }
104        Ok(())
105    }
106
107    fn render_select(&mut self, select: &Select) -> Result<(), RenderError> {
108        if !select.ctes.is_empty() {
109            self.sql.push_str("WITH ");
110            if select.recursive {
111                self.sql.push_str("RECURSIVE ");
112            }
113            for (index, cte) in select.ctes.iter().enumerate() {
114                if index > 0 {
115                    self.sql.push_str(", ");
116                }
117                self.identifier(&cte.name)?;
118                if !cte.columns.is_empty() {
119                    self.sql.push_str(" (");
120                    self.identifier_list(&cte.columns)?;
121                    self.sql.push(')');
122                }
123                self.sql.push_str(" AS (");
124                self.render_select(&cte.query)?;
125                self.sql.push(')');
126            }
127            self.sql.push(' ');
128        }
129
130        self.sql.push_str("SELECT ");
131        if !select.distinct_on.is_empty() {
132            self.sql.push_str("DISTINCT ON (");
133            self.expression_list(&select.distinct_on)?;
134            self.sql.push_str(") ");
135        } else if select.distinct {
136            self.sql.push_str("DISTINCT ");
137        }
138        if select.projection.is_empty() {
139            return Err(RenderError::InvalidOperation(
140                "SELECT requires at least one projection".to_string(),
141            ));
142        }
143        self.projection_list(&select.projection)?;
144        if let Some(from) = &select.from {
145            self.sql.push_str(" FROM ");
146            self.render_relation(from)?;
147        }
148        if let Some(filter) = &select.filter {
149            self.sql.push_str(" WHERE ");
150            self.render_expression(filter)?;
151        }
152        if let Some(grouping) = &select.group_by {
153            self.sql.push_str(" GROUP BY ");
154            self.render_grouping(grouping)?;
155        }
156        if let Some(having) = &select.having {
157            self.sql.push_str(" HAVING ");
158            self.render_expression(having)?;
159        }
160        if !select.windows.is_empty() {
161            self.sql.push_str(" WINDOW ");
162            for (index, window) in select.windows.iter().enumerate() {
163                if index > 0 {
164                    self.sql.push_str(", ");
165                }
166                self.identifier(&window.name)?;
167                self.sql.push_str(" AS (");
168                self.render_window_specification(&window.specification)?;
169                self.sql.push(')');
170            }
171        }
172        for arm in &select.set_operations {
173            self.sql.push(' ');
174            self.sql.push_str(match arm.operator {
175                SetOperator::Union => "UNION",
176                SetOperator::UnionAll => "UNION ALL",
177                SetOperator::Intersect => "INTERSECT",
178                SetOperator::Except => "EXCEPT",
179            });
180            self.sql.push(' ');
181            self.render_select(&arm.query)?;
182        }
183        if !select.order_by.is_empty() {
184            self.sql.push_str(" ORDER BY ");
185            self.order_by_list(&select.order_by)?;
186        }
187        if let Some(limit) = select.limit {
188            write!(&mut self.sql, " LIMIT {limit}").unwrap();
189        }
190        if let Some(offset) = select.offset {
191            write!(&mut self.sql, " OFFSET {offset}").unwrap();
192        }
193        Ok(())
194    }
195
196    fn projection_list(&mut self, projections: &[Projection]) -> Result<(), RenderError> {
197        for (index, projection) in projections.iter().enumerate() {
198            if index > 0 {
199                self.sql.push_str(", ");
200            }
201            self.render_expression(&projection.expression)?;
202            if let Some(alias) = &projection.alias {
203                self.sql.push_str(" AS ");
204                self.identifier(alias)?;
205            }
206        }
207        Ok(())
208    }
209
210    fn render_relation(&mut self, relation: &Relation) -> Result<(), RenderError> {
211        match relation {
212            Relation::Table { name, alias } | Relation::Cte { name, alias } => {
213                self.qualified_identifier(name)?;
214                if let Some(alias) = alias {
215                    self.sql.push_str(" AS ");
216                    self.identifier(alias)?;
217                }
218            }
219            Relation::Derived { query, alias } => {
220                self.sql.push('(');
221                self.render_select(query)?;
222                self.sql.push_str(") AS ");
223                self.identifier(alias)?;
224            }
225            Relation::Values {
226                rows,
227                alias,
228                columns,
229            } => {
230                if rows.is_empty() {
231                    return Err(RenderError::InvalidOperation(
232                        "VALUES relation requires at least one row".to_string(),
233                    ));
234                }
235                self.sql.push_str("(VALUES ");
236                self.expression_rows(rows)?;
237                self.sql.push_str(") AS ");
238                self.identifier(alias)?;
239                if !columns.is_empty() {
240                    self.sql.push_str(" (");
241                    self.identifier_list(columns)?;
242                    self.sql.push(')');
243                }
244            }
245            Relation::Join {
246                left,
247                right,
248                kind,
249                on,
250            } => {
251                self.render_relation(left)?;
252                self.sql.push(' ');
253                self.sql.push_str(match kind {
254                    JoinKind::Inner => "INNER JOIN",
255                    JoinKind::Left => "LEFT JOIN",
256                    JoinKind::Right => "RIGHT JOIN",
257                    JoinKind::Full => "FULL JOIN",
258                    JoinKind::Cross => "CROSS JOIN",
259                });
260                self.sql.push(' ');
261                self.render_relation(right)?;
262                if *kind == JoinKind::Cross {
263                    if on.is_some() {
264                        return Err(RenderError::InvalidOperation(
265                            "CROSS JOIN cannot have an ON predicate".to_string(),
266                        ));
267                    }
268                } else {
269                    let on = on.as_ref().ok_or_else(|| {
270                        RenderError::InvalidOperation(
271                            "non-CROSS JOIN requires an ON predicate".to_string(),
272                        )
273                    })?;
274                    self.sql.push_str(" ON ");
275                    self.render_expression(on)?;
276                }
277            }
278        }
279        Ok(())
280    }
281
282    fn render_expression(&mut self, expression: &Expression) -> Result<(), RenderError> {
283        match expression {
284            Expression::Column { column } => {
285                if let Some(relation) = &column.relation {
286                    self.identifier(relation)?;
287                    self.sql.push('.');
288                }
289                self.identifier(&column.name)?;
290            }
291            Expression::Literal { value } => self.parameter(value.clone()),
292            Expression::Star { relation } => {
293                if let Some(relation) = relation {
294                    self.identifier(relation)?;
295                    self.sql.push('.');
296                }
297                self.sql.push('*');
298            }
299            Expression::Unary {
300                operator,
301                expression,
302            } => {
303                self.sql.push_str(match operator {
304                    UnaryOperator::Not => "NOT ",
305                    UnaryOperator::Negate => "-",
306                    UnaryOperator::Positive => "+",
307                });
308                self.sql.push('(');
309                self.render_expression(expression)?;
310                self.sql.push(')');
311            }
312            Expression::Binary {
313                left,
314                operator,
315                right,
316            } => {
317                self.sql.push('(');
318                self.render_expression(left)?;
319                self.sql.push(' ');
320                self.sql.push_str(match operator {
321                    BinaryOperator::Eq => "=",
322                    BinaryOperator::Ne => "<>",
323                    BinaryOperator::Lt => "<",
324                    BinaryOperator::Lte => "<=",
325                    BinaryOperator::Gt => ">",
326                    BinaryOperator::Gte => ">=",
327                    BinaryOperator::And => "AND",
328                    BinaryOperator::Or => "OR",
329                    BinaryOperator::Xor => "XOR",
330                    BinaryOperator::Add => "+",
331                    BinaryOperator::Subtract => "-",
332                    BinaryOperator::Multiply => "*",
333                    BinaryOperator::Divide => "/",
334                    BinaryOperator::Modulo => "%",
335                    BinaryOperator::Like => "LIKE",
336                    BinaryOperator::NotLike => "NOT LIKE",
337                    BinaryOperator::Glob => "GLOB",
338                    BinaryOperator::Regexp => "REGEXP",
339                    BinaryOperator::IsDistinctFrom => "IS DISTINCT FROM",
340                    BinaryOperator::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
341                });
342                self.sql.push(' ');
343                self.render_expression(right)?;
344                self.sql.push(')');
345            }
346            Expression::Function { name, arguments } => {
347                self.function_name(name)?;
348                self.sql.push('(');
349                self.expression_list(arguments)?;
350                self.sql.push(')');
351            }
352            Expression::Aggregate {
353                name,
354                arguments,
355                distinct,
356                filter,
357                order_by,
358            } => {
359                self.function_name(name)?;
360                self.sql.push('(');
361                if *distinct {
362                    self.sql.push_str("DISTINCT ");
363                }
364                self.expression_list(arguments)?;
365                if !order_by.is_empty() {
366                    self.sql.push_str(" ORDER BY ");
367                    self.order_by_list(order_by)?;
368                }
369                self.sql.push(')');
370                if let Some(filter) = filter {
371                    self.sql.push_str(" FILTER (WHERE ");
372                    self.render_expression(filter)?;
373                    self.sql.push(')');
374                }
375            }
376            Expression::Window {
377                function,
378                specification,
379            } => {
380                self.render_expression(function)?;
381                self.sql.push_str(" OVER ");
382                if let Some(name) = &specification.name {
383                    if specification.partition_by.is_empty()
384                        && specification.order_by.is_empty()
385                        && specification.frame.is_none()
386                    {
387                        self.identifier(name)?;
388                        return Ok(());
389                    }
390                }
391                self.sql.push('(');
392                self.render_window_specification(specification)?;
393                self.sql.push(')');
394            }
395            Expression::Cast {
396                expression,
397                data_type,
398            } => {
399                self.sql.push_str("CAST(");
400                self.render_expression(expression)?;
401                self.sql.push_str(" AS ");
402                self.render_data_type(data_type)?;
403                self.sql.push(')');
404            }
405            Expression::Case {
406                operand,
407                branches,
408                otherwise,
409            } => {
410                self.sql.push_str("CASE");
411                if let Some(operand) = operand {
412                    self.sql.push(' ');
413                    self.render_expression(operand)?;
414                }
415                for branch in branches {
416                    self.sql.push_str(" WHEN ");
417                    self.render_expression(&branch.when)?;
418                    self.sql.push_str(" THEN ");
419                    self.render_expression(&branch.then)?;
420                }
421                if let Some(otherwise) = otherwise {
422                    self.sql.push_str(" ELSE ");
423                    self.render_expression(otherwise)?;
424                }
425                self.sql.push_str(" END");
426            }
427            Expression::IsNull {
428                expression,
429                negated,
430            } => {
431                self.sql.push('(');
432                self.render_expression(expression)?;
433                self.sql.push_str(if *negated {
434                    " IS NOT NULL)"
435                } else {
436                    " IS NULL)"
437                });
438            }
439            Expression::Between {
440                expression,
441                lower,
442                upper,
443                negated,
444            } => {
445                self.sql.push('(');
446                self.render_expression(expression)?;
447                self.sql.push_str(if *negated {
448                    " NOT BETWEEN "
449                } else {
450                    " BETWEEN "
451                });
452                self.render_expression(lower)?;
453                self.sql.push_str(" AND ");
454                self.render_expression(upper)?;
455                self.sql.push(')');
456            }
457            Expression::InList {
458                expression,
459                values,
460                negated,
461            } => {
462                if values.is_empty() {
463                    return Err(RenderError::InvalidOperation(
464                        "IN list cannot be empty".to_string(),
465                    ));
466                }
467                self.sql.push('(');
468                self.render_expression(expression)?;
469                self.sql
470                    .push_str(if *negated { " NOT IN (" } else { " IN (" });
471                self.expression_list(values)?;
472                self.sql.push_str("))");
473            }
474            Expression::InSubquery {
475                expression,
476                query,
477                negated,
478            } => {
479                self.sql.push('(');
480                self.render_expression(expression)?;
481                self.sql
482                    .push_str(if *negated { " NOT IN (" } else { " IN (" });
483                self.render_select(query)?;
484                self.sql.push_str("))");
485            }
486            Expression::Exists { query, negated } => {
487                if *negated {
488                    self.sql.push_str("NOT ");
489                }
490                self.sql.push_str("EXISTS (");
491                self.render_select(query)?;
492                self.sql.push(')');
493            }
494            Expression::ScalarSubquery { query } => {
495                self.sql.push('(');
496                self.render_select(query)?;
497                self.sql.push(')');
498            }
499            Expression::Tuple { values } => {
500                self.sql.push('(');
501                self.expression_list(values)?;
502                self.sql.push(')');
503            }
504            Expression::Navigation { root, path } => {
505                if path.is_empty() {
506                    return Err(RenderError::InvalidOperation(
507                        "navigation path requires at least one segment".to_string(),
508                    ));
509                }
510                self.identifier(root)?;
511                for segment in path {
512                    self.sql.push('.');
513                    self.identifier(segment)?;
514                }
515            }
516            Expression::Grouping { expressions } => {
517                self.sql.push_str("GROUPING(");
518                self.expression_list(expressions)?;
519                self.sql.push(')');
520            }
521        }
522        Ok(())
523    }
524
525    fn render_grouping(&mut self, grouping: &Grouping) -> Result<(), RenderError> {
526        match grouping {
527            Grouping::Expressions { expressions } => self.expression_list(expressions),
528            Grouping::Rollup { expressions } => {
529                self.sql.push_str("ROLLUP (");
530                self.expression_list(expressions)?;
531                self.sql.push(')');
532                Ok(())
533            }
534            Grouping::Cube { expressions } => {
535                self.sql.push_str("CUBE (");
536                self.expression_list(expressions)?;
537                self.sql.push(')');
538                Ok(())
539            }
540            Grouping::Sets { sets } => {
541                self.sql.push_str("GROUPING SETS (");
542                for (index, set) in sets.iter().enumerate() {
543                    if index > 0 {
544                        self.sql.push_str(", ");
545                    }
546                    self.sql.push('(');
547                    self.expression_list(set)?;
548                    self.sql.push(')');
549                }
550                self.sql.push(')');
551                Ok(())
552            }
553        }
554    }
555
556    fn render_window_specification(
557        &mut self,
558        specification: &WindowSpecification,
559    ) -> Result<(), RenderError> {
560        let mut wrote = false;
561        if let Some(name) = &specification.name {
562            self.identifier(name)?;
563            wrote = true;
564        }
565        if !specification.partition_by.is_empty() {
566            if wrote {
567                self.sql.push(' ');
568            }
569            self.sql.push_str("PARTITION BY ");
570            self.expression_list(&specification.partition_by)?;
571            wrote = true;
572        }
573        if !specification.order_by.is_empty() {
574            if wrote {
575                self.sql.push(' ');
576            }
577            self.sql.push_str("ORDER BY ");
578            self.order_by_list(&specification.order_by)?;
579            wrote = true;
580        }
581        if let Some(frame) = &specification.frame {
582            if wrote {
583                self.sql.push(' ');
584            }
585            self.sql.push_str(match frame.unit {
586                WindowFrameUnit::Rows => "ROWS ",
587                WindowFrameUnit::Range => "RANGE ",
588            });
589            if let Some(end) = &frame.end {
590                self.sql.push_str("BETWEEN ");
591                self.render_frame_bound(&frame.start);
592                self.sql.push_str(" AND ");
593                self.render_frame_bound(end);
594            } else {
595                self.render_frame_bound(&frame.start);
596            }
597        }
598        Ok(())
599    }
600
601    fn render_frame_bound(&mut self, bound: &WindowFrameBound) {
602        match bound {
603            WindowFrameBound::UnboundedPreceding => self.sql.push_str("UNBOUNDED PRECEDING"),
604            WindowFrameBound::Preceding(offset) => {
605                write!(&mut self.sql, "{offset} PRECEDING").unwrap()
606            }
607            WindowFrameBound::CurrentRow => self.sql.push_str("CURRENT ROW"),
608            WindowFrameBound::Following(offset) => {
609                write!(&mut self.sql, "{offset} FOLLOWING").unwrap()
610            }
611            WindowFrameBound::UnboundedFollowing => self.sql.push_str("UNBOUNDED FOLLOWING"),
612        }
613    }
614
615    fn order_by_list(&mut self, order: &[OrderBy]) -> Result<(), RenderError> {
616        for (index, item) in order.iter().enumerate() {
617            if index > 0 {
618                self.sql.push_str(", ");
619            }
620            self.render_expression(&item.expression)?;
621            self.sql.push_str(match item.direction {
622                SortDirection::Asc => " ASC",
623                SortDirection::Desc => " DESC",
624            });
625            if let Some(nulls) = item.nulls {
626                self.sql.push_str(match nulls {
627                    NullPlacement::First => " NULLS FIRST",
628                    NullPlacement::Last => " NULLS LAST",
629                });
630            }
631        }
632        Ok(())
633    }
634
635    fn render_insert(&mut self, insert: &Insert) -> Result<(), RenderError> {
636        if insert.columns.is_empty() {
637            return Err(RenderError::InvalidOperation(
638                "INSERT requires explicit columns".to_string(),
639            ));
640        }
641        if insert.rows.is_empty() == insert.source.is_none() {
642            return Err(RenderError::InvalidOperation(
643                "INSERT requires exactly one of VALUES rows or SELECT source".to_string(),
644            ));
645        }
646        self.sql.push_str("INSERT INTO ");
647        self.qualified_identifier(&insert.table)?;
648        self.sql.push_str(" (");
649        self.identifier_list(&insert.columns)?;
650        self.sql.push_str(") ");
651        if !insert.rows.is_empty() {
652            if insert
653                .rows
654                .iter()
655                .any(|row| row.len() != insert.columns.len())
656            {
657                return Err(RenderError::InvalidOperation(
658                    "INSERT row width differs from column count".to_string(),
659                ));
660            }
661            self.sql.push_str("VALUES ");
662            self.expression_rows(&insert.rows)?;
663        } else if let Some(source) = &insert.source {
664            self.render_select(source)?;
665        }
666        self.render_returning(&insert.returning)
667    }
668
669    fn render_upsert(&mut self, upsert: &Upsert) -> Result<(), RenderError> {
670        self.render_insert(&Insert {
671            returning: Vec::new(),
672            ..upsert.insert.clone()
673        })?;
674        if upsert.conflict_columns.is_empty() {
675            return Err(RenderError::InvalidOperation(
676                "UPSERT requires explicit conflict columns".to_string(),
677            ));
678        }
679        self.sql.push_str(" ON CONFLICT (");
680        self.identifier_list(&upsert.conflict_columns)?;
681        if upsert.assignments.is_empty() {
682            self.sql.push_str(") DO NOTHING");
683        } else {
684            self.sql.push_str(") DO UPDATE SET ");
685            self.assignment_list(&upsert.assignments)?;
686        }
687        self.render_returning(&upsert.insert.returning)
688    }
689
690    fn render_update(&mut self, update: &Update) -> Result<(), RenderError> {
691        if update.assignments.is_empty() {
692            return Err(RenderError::InvalidOperation(
693                "UPDATE requires at least one assignment".to_string(),
694            ));
695        }
696        if update.alias.is_some() || update.from.is_some() {
697            return Err(RenderError::Unsupported(
698                "UPDATE aliases and UPDATE ... FROM are not part of the current RadixDB SQL grammar"
699                    .to_string(),
700            ));
701        }
702        self.sql.push_str("UPDATE ");
703        self.qualified_identifier(&update.table)?;
704        self.sql.push_str(" SET ");
705        self.assignment_list(&update.assignments)?;
706        if let Some(filter) = &update.filter {
707            self.sql.push_str(" WHERE ");
708            self.render_expression(filter)?;
709        }
710        self.render_returning(&update.returning)
711    }
712
713    fn render_delete(&mut self, delete: &Delete) -> Result<(), RenderError> {
714        if delete.filter.is_none() && !delete.all_rows {
715            return Err(RenderError::InvalidOperation(
716                "unguarded DELETE requires explicit all_rows".to_string(),
717            ));
718        }
719        if delete.using.is_some() {
720            return Err(RenderError::Unsupported(
721                "DELETE ... USING is not part of the current RadixDB SQL grammar".to_string(),
722            ));
723        }
724        self.sql.push_str("DELETE FROM ");
725        self.qualified_identifier(&delete.table)?;
726        if let Some(alias) = &delete.alias {
727            self.sql.push_str(" AS ");
728            self.identifier(alias)?;
729        }
730        if let Some(filter) = &delete.filter {
731            self.sql.push_str(" WHERE ");
732            self.render_expression(filter)?;
733        }
734        self.render_returning(&delete.returning)
735    }
736
737    fn assignment_list(&mut self, assignments: &[Assignment]) -> Result<(), RenderError> {
738        for (index, assignment) in assignments.iter().enumerate() {
739            if index > 0 {
740                self.sql.push_str(", ");
741            }
742            self.identifier(&assignment.column)?;
743            self.sql.push_str(" = ");
744            self.render_expression(&assignment.value)?;
745        }
746        Ok(())
747    }
748
749    fn render_returning(&mut self, returning: &[Projection]) -> Result<(), RenderError> {
750        if !returning.is_empty() {
751            self.sql.push_str(" RETURNING ");
752            self.projection_list(returning)?;
753        }
754        Ok(())
755    }
756
757    fn render_ddl(&mut self, operation: &DdlOperation) -> Result<(), RenderError> {
758        match operation {
759            DdlOperation::CreateTable {
760                table,
761                if_not_exists,
762                columns,
763                constraints,
764            } => {
765                if columns.is_empty() {
766                    return Err(RenderError::InvalidOperation(
767                        "CREATE TABLE requires columns".to_string(),
768                    ));
769                }
770                self.sql.push_str("CREATE TABLE ");
771                if *if_not_exists {
772                    self.sql.push_str("IF NOT EXISTS ");
773                }
774                self.qualified_identifier(table)?;
775                self.sql.push_str(" (");
776                for (index, column) in columns.iter().enumerate() {
777                    if index > 0 {
778                        self.sql.push_str(", ");
779                    }
780                    self.render_column_definition(column)?;
781                }
782                for constraint in constraints {
783                    self.sql.push_str(", ");
784                    self.render_constraint(constraint)?;
785                }
786                self.sql.push(')');
787            }
788            DdlOperation::CreateTableAs {
789                table,
790                if_not_exists,
791                query,
792            } => {
793                self.sql.push_str("CREATE TABLE ");
794                if *if_not_exists {
795                    self.sql.push_str("IF NOT EXISTS ");
796                }
797                self.qualified_identifier(table)?;
798                self.sql.push_str(" AS ");
799                self.render_select(query)?;
800            }
801            DdlOperation::AlterTable { table, action } => {
802                self.sql.push_str("ALTER TABLE ");
803                self.qualified_identifier(table)?;
804                self.sql.push(' ');
805                self.render_alter_action(action)?;
806            }
807            DdlOperation::DropTable { table, if_exists } => {
808                self.sql.push_str("DROP TABLE ");
809                if *if_exists {
810                    self.sql.push_str("IF EXISTS ");
811                }
812                self.qualified_identifier(table)?;
813            }
814            DdlOperation::TruncateTable { table } => {
815                self.sql.push_str("TRUNCATE TABLE ");
816                self.qualified_identifier(table)?;
817            }
818            DdlOperation::CreateIndex { index } => self.render_create_index(index)?,
819            DdlOperation::DropIndex {
820                table,
821                index,
822                if_exists,
823            } => {
824                self.sql.push_str("DROP INDEX ");
825                if *if_exists {
826                    self.sql.push_str("IF EXISTS ");
827                }
828                self.identifier(index)?;
829                self.sql.push_str(" ON ");
830                self.qualified_identifier(table)?;
831            }
832            DdlOperation::AlterIndex { index, new_name } => {
833                self.sql.push_str("ALTER INDEX ");
834                self.identifier(index)?;
835                self.sql.push_str(" RENAME TO ");
836                self.identifier(new_name)?;
837            }
838        }
839        Ok(())
840    }
841
842    fn render_column_definition(&mut self, column: &ColumnDefinition) -> Result<(), RenderError> {
843        self.identifier(&column.name)?;
844        self.sql.push(' ');
845        self.render_data_type(&column.data_type)?;
846        if column.primary_key {
847            self.sql.push_str(" PRIMARY KEY");
848        }
849        if !column.nullable && !column.primary_key {
850            self.sql.push_str(" NOT NULL");
851        }
852        if column.unique && !column.primary_key {
853            self.sql.push_str(" UNIQUE");
854        }
855        if column.auto_increment {
856            self.sql.push_str(" AUTO_INCREMENT");
857        }
858        if let Some(default) = &column.default {
859            self.sql.push_str(" DEFAULT ");
860            self.render_expression(default)?;
861        }
862        if let Some(check) = &column.check {
863            self.sql.push_str(" CHECK (");
864            self.render_expression(check)?;
865            self.sql.push(')');
866        }
867        if let Some(reference) = &column.reference {
868            self.sql.push_str(" REFERENCES ");
869            self.qualified_identifier(&reference.table)?;
870            self.sql.push_str(" (");
871            self.identifier(&reference.column)?;
872            self.sql.push(')');
873            self.render_fk_actions(reference.on_delete, reference.on_update);
874        }
875        Ok(())
876    }
877
878    fn render_constraint(
879        &mut self,
880        constraint: &ConstraintDefinitionIr,
881    ) -> Result<(), RenderError> {
882        match constraint {
883            ConstraintDefinitionIr::PrimaryKey { columns } => {
884                self.sql.push_str("PRIMARY KEY (");
885                self.identifier_list(columns)?;
886                self.sql.push(')');
887            }
888            ConstraintDefinitionIr::Unique { columns } => {
889                self.sql.push_str("UNIQUE (");
890                self.identifier_list(columns)?;
891                self.sql.push(')');
892            }
893            ConstraintDefinitionIr::ForeignKey {
894                columns,
895                referenced_table,
896                referenced_columns,
897                on_delete,
898                on_update,
899            } => {
900                self.sql.push_str("FOREIGN KEY (");
901                self.identifier_list(columns)?;
902                self.sql.push_str(") REFERENCES ");
903                self.qualified_identifier(referenced_table)?;
904                self.sql.push_str(" (");
905                self.identifier_list(referenced_columns)?;
906                self.sql.push(')');
907                self.render_fk_actions(*on_delete, *on_update);
908            }
909            ConstraintDefinitionIr::Check { expression } => {
910                self.sql.push_str("CHECK (");
911                self.render_expression(expression)?;
912                self.sql.push(')');
913            }
914        }
915        Ok(())
916    }
917
918    fn render_fk_actions(
919        &mut self,
920        on_delete: ForeignKeyActionDescriptor,
921        on_update: ForeignKeyActionDescriptor,
922    ) {
923        self.sql.push_str(" ON DELETE ");
924        self.sql.push_str(fk_action(on_delete));
925        self.sql.push_str(" ON UPDATE ");
926        self.sql.push_str(fk_action(on_update));
927    }
928
929    fn render_alter_action(&mut self, action: &AlterTableAction) -> Result<(), RenderError> {
930        match action {
931            AlterTableAction::AddColumn { column } => {
932                self.sql.push_str("ADD COLUMN ");
933                self.render_column_definition(column)?;
934            }
935            AlterTableAction::ModifyColumn { column } => {
936                self.sql.push_str("MODIFY COLUMN ");
937                self.render_column_definition(column)?;
938            }
939            AlterTableAction::DropColumn { column } => {
940                self.sql.push_str("DROP COLUMN ");
941                self.identifier(column)?;
942            }
943            AlterTableAction::RenameColumn { from, to } => {
944                self.sql.push_str("RENAME COLUMN ");
945                self.identifier(from)?;
946                self.sql.push_str(" TO ");
947                self.identifier(to)?;
948            }
949            AlterTableAction::RenameTable { to } => {
950                self.sql.push_str("RENAME TO ");
951                self.qualified_identifier(to)?;
952            }
953            AlterTableAction::AddConstraint { constraint } => {
954                self.sql.push_str("ADD CONSTRAINT ");
955                self.render_constraint(constraint)?;
956            }
957            AlterTableAction::DropConstraint { name, if_exists } => {
958                self.sql.push_str("DROP CONSTRAINT ");
959                if *if_exists {
960                    self.sql.push_str("IF EXISTS ");
961                }
962                self.identifier(name)?;
963            }
964        }
965        Ok(())
966    }
967
968    fn render_create_index(&mut self, index: &IndexDefinition) -> Result<(), RenderError> {
969        self.sql.push_str("CREATE ");
970        if index.unique {
971            self.sql.push_str("UNIQUE ");
972        }
973        self.sql.push_str("INDEX ");
974        if index.if_not_exists {
975            self.sql.push_str("IF NOT EXISTS ");
976        }
977        self.identifier(&index.name)?;
978        self.sql.push_str(" ON ");
979        self.qualified_identifier(&index.table)?;
980        self.sql.push_str(" (");
981        self.identifier_list(&index.columns)?;
982        self.sql.push(')');
983        if let Some(method) = &index.method {
984            self.sql.push_str(" USING ");
985            self.identifier(method)?;
986        }
987        if !index.options.is_empty() {
988            self.sql.push_str(" WITH (");
989            for (position, (name, value)) in index.options.iter().enumerate() {
990                if position > 0 {
991                    self.sql.push_str(", ");
992                }
993                self.identifier(name)?;
994                self.sql.push_str(" = ");
995                self.parameter(value.clone());
996            }
997            self.sql.push(')');
998        }
999        if let Some(predicate) = &index.predicate {
1000            self.sql.push_str(" WHERE ");
1001            self.render_expression(predicate)?;
1002        }
1003        Ok(())
1004    }
1005
1006    fn render_transaction(&mut self, operation: &TransactionOperation) -> Result<(), RenderError> {
1007        match operation {
1008            TransactionOperation::Begin => self.sql.push_str("BEGIN"),
1009            TransactionOperation::Commit => self.sql.push_str("COMMIT"),
1010            TransactionOperation::Rollback => self.sql.push_str("ROLLBACK"),
1011            TransactionOperation::Savepoint { name } => {
1012                self.sql.push_str("SAVEPOINT ");
1013                self.identifier(name)?;
1014            }
1015            TransactionOperation::RollbackToSavepoint { name } => {
1016                self.sql.push_str("ROLLBACK TO SAVEPOINT ");
1017                self.identifier(name)?;
1018            }
1019            TransactionOperation::ReleaseSavepoint { name } => {
1020                self.sql.push_str("RELEASE SAVEPOINT ");
1021                self.identifier(name)?;
1022            }
1023        }
1024        Ok(())
1025    }
1026
1027    fn render_data_type(&mut self, data_type: &DataTypeDescriptor) -> Result<(), RenderError> {
1028        match data_type {
1029            DataTypeDescriptor::Null => {
1030                return Err(RenderError::InvalidOperation(
1031                    "NULL is not a schema column type".to_string(),
1032                ))
1033            }
1034            DataTypeDescriptor::Integer => self.sql.push_str("INTEGER"),
1035            DataTypeDescriptor::Float => self.sql.push_str("FLOAT"),
1036            DataTypeDescriptor::Text => self.sql.push_str("TEXT"),
1037            DataTypeDescriptor::Boolean => self.sql.push_str("BOOLEAN"),
1038            DataTypeDescriptor::Timestamp => self.sql.push_str("TIMESTAMP"),
1039            DataTypeDescriptor::Date => self.sql.push_str("DATE"),
1040            DataTypeDescriptor::Json => self.sql.push_str("JSON"),
1041            DataTypeDescriptor::Uuid => self.sql.push_str("UUID"),
1042            DataTypeDescriptor::Bytes => self.sql.push_str("BYTES"),
1043            DataTypeDescriptor::Decimal { precision, scale } => match (precision, scale) {
1044                (None, None) => self.sql.push_str("DECIMAL"),
1045                (Some(precision), scale) => {
1046                    let scale = scale.unwrap_or(0);
1047                    if *precision == 0 || *precision > 38 || scale > *precision {
1048                        return Err(RenderError::InvalidOperation(format!(
1049                            "invalid DECIMAL({precision},{scale})"
1050                        )));
1051                    }
1052                    write!(&mut self.sql, "DECIMAL({precision},{scale})").unwrap();
1053                }
1054                (None, Some(_)) => {
1055                    return Err(RenderError::InvalidOperation(
1056                        "DECIMAL scale requires precision".to_string(),
1057                    ))
1058                }
1059            },
1060            DataTypeDescriptor::Vector { dimensions } => {
1061                if *dimensions == 0 {
1062                    return Err(RenderError::InvalidOperation(
1063                        "VECTOR dimensions must be positive".to_string(),
1064                    ));
1065                }
1066                write!(&mut self.sql, "VECTOR({dimensions})").unwrap();
1067            }
1068        }
1069        Ok(())
1070    }
1071
1072    fn expression_list(&mut self, expressions: &[Expression]) -> Result<(), RenderError> {
1073        for (index, expression) in expressions.iter().enumerate() {
1074            if index > 0 {
1075                self.sql.push_str(", ");
1076            }
1077            self.render_expression(expression)?;
1078        }
1079        Ok(())
1080    }
1081
1082    fn expression_rows(&mut self, rows: &[Vec<Expression>]) -> Result<(), RenderError> {
1083        for (index, row) in rows.iter().enumerate() {
1084            if index > 0 {
1085                self.sql.push_str(", ");
1086            }
1087            self.sql.push('(');
1088            self.expression_list(row)?;
1089            self.sql.push(')');
1090        }
1091        Ok(())
1092    }
1093
1094    fn identifier_list(&mut self, identifiers: &[String]) -> Result<(), RenderError> {
1095        if identifiers.is_empty() {
1096            return Err(RenderError::InvalidOperation(
1097                "identifier list cannot be empty".to_string(),
1098            ));
1099        }
1100        for (index, identifier) in identifiers.iter().enumerate() {
1101            if index > 0 {
1102                self.sql.push_str(", ");
1103            }
1104            self.identifier(identifier)?;
1105        }
1106        Ok(())
1107    }
1108
1109    fn identifier(&mut self, identifier: &str) -> Result<(), RenderError> {
1110        if identifier.is_empty() || identifier.contains(['\0', '.']) {
1111            return Err(RenderError::InvalidIdentifier(identifier.to_string()));
1112        }
1113        self.sql.push('"');
1114        self.sql.push_str(&identifier.replace('"', "\"\""));
1115        self.sql.push('"');
1116        Ok(())
1117    }
1118
1119    fn qualified_identifier(&mut self, identifier: &str) -> Result<(), RenderError> {
1120        let parts = identifier.split('.').collect::<Vec<_>>();
1121        if parts.is_empty() || parts.iter().any(|part| part.is_empty()) {
1122            return Err(RenderError::InvalidIdentifier(identifier.to_string()));
1123        }
1124        for (index, part) in parts.iter().enumerate() {
1125            if index > 0 {
1126                self.sql.push('.');
1127            }
1128            self.identifier(part)?;
1129        }
1130        Ok(())
1131    }
1132
1133    fn function_name(&mut self, name: &str) -> Result<(), RenderError> {
1134        self.qualified_identifier(name)
1135    }
1136
1137    fn parameter(&mut self, value: TypedValue) {
1138        self.parameters.push(value);
1139        write!(&mut self.sql, "${}", self.parameters.len()).unwrap();
1140    }
1141}
1142
1143fn fk_action(action: ForeignKeyActionDescriptor) -> &'static str {
1144    match action {
1145        ForeignKeyActionDescriptor::Restrict => "RESTRICT",
1146        ForeignKeyActionDescriptor::Cascade => "CASCADE",
1147        ForeignKeyActionDescriptor::SetNull => "SET NULL",
1148        ForeignKeyActionDescriptor::NoAction => "NO ACTION",
1149    }
1150}
1151
1152fn shape_fingerprint(
1153    sql: &str,
1154    parameters: &[TypedValue],
1155    result: &[ResultColumnDescriptor],
1156) -> String {
1157    let parameter_types = parameters
1158        .iter()
1159        .map(TypedValue::data_type)
1160        .collect::<Vec<_>>();
1161    let bytes = serde_json::to_vec(&(sql, parameter_types, result))
1162        .expect("shape fingerprint inputs are serializable");
1163    let digest = Sha256::digest(bytes);
1164    let mut fingerprint = String::with_capacity(digest.len() * 2);
1165    for byte in digest {
1166        write!(&mut fingerprint, "{byte:02x}").unwrap();
1167    }
1168    fingerprint
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173    use super::*;
1174
1175    fn col(relation: &str, name: &str) -> Expression {
1176        Expression::Column {
1177            column: ColumnRef::qualified(relation, name),
1178        }
1179    }
1180
1181    #[test]
1182    fn complex_select_renders_navigation_grouping_window_and_ordered_parameters() {
1183        let query = Select {
1184            projection: vec![
1185                Projection {
1186                    expression: Expression::Navigation {
1187                        root: "p".to_string(),
1188                        path: vec!["fio".to_string(), "name".to_string()],
1189                    },
1190                    alias: Some("fio_name".to_string()),
1191                },
1192                Projection {
1193                    expression: Expression::Window {
1194                        function: Box::new(Expression::Function {
1195                            name: "row_number".to_string(),
1196                            arguments: Vec::new(),
1197                        }),
1198                        specification: WindowSpecification {
1199                            partition_by: vec![col("p", "department_id")],
1200                            order_by: vec![OrderBy {
1201                                expression: col("p", "name"),
1202                                direction: SortDirection::Asc,
1203                                nulls: Some(NullPlacement::Last),
1204                            }],
1205                            ..WindowSpecification::default()
1206                        },
1207                    },
1208                    alias: Some("rn".to_string()),
1209                },
1210            ],
1211            from: Some(Relation::Table {
1212                name: "people".to_string(),
1213                alias: Some("p".to_string()),
1214            }),
1215            filter: Some(Expression::Binary {
1216                left: Box::new(col("p", "name")),
1217                operator: BinaryOperator::Like,
1218                right: Box::new(Expression::literal(TypedValue::Text("Ivan%".to_string()))),
1219            }),
1220            group_by: Some(Grouping::Cube {
1221                expressions: vec![col("p", "department_id")],
1222            }),
1223            order_by: vec![OrderBy {
1224                expression: col("p", "name"),
1225                direction: SortDirection::Desc,
1226                nulls: Some(NullPlacement::First),
1227            }],
1228            limit: Some(10),
1229            ..Select::default()
1230        };
1231        let compiled = IrDocument::new(Operation::Select { query })
1232            .to_sql()
1233            .unwrap();
1234        assert_eq!(compiled.parameters, vec![TypedValue::Text("Ivan%".into())]);
1235        assert!(compiled.sql.contains("\"p\".\"fio\".\"name\""));
1236        assert!(compiled.sql.contains("GROUP BY CUBE"));
1237        assert!(compiled.sql.contains("$1"));
1238        assert_eq!(compiled.shape_fingerprint.len(), 64);
1239    }
1240
1241    #[test]
1242    fn renderer_never_interpolates_values_and_rejects_unsafe_mutations() {
1243        let insert = Insert {
1244            table: "people".to_string(),
1245            columns: vec!["name".to_string()],
1246            rows: vec![vec![Expression::literal(TypedValue::Text(
1247                "x'); DROP TABLE people; --".to_string(),
1248            ))]],
1249            source: None,
1250            returning: vec![Projection {
1251                expression: Expression::Star { relation: None },
1252                alias: None,
1253            }],
1254        };
1255        let compiled = SqlRenderer::compile(&Operation::Insert { statement: insert }).unwrap();
1256        assert!(!compiled.sql.contains("DROP TABLE"));
1257        assert!(compiled.sql.contains("$1"));
1258
1259        let delete = Delete {
1260            table: "people".to_string(),
1261            alias: None,
1262            using: None,
1263            filter: None,
1264            all_rows: false,
1265            returning: Vec::new(),
1266        };
1267        assert!(SqlRenderer::compile(&Operation::Delete { statement: delete }).is_err());
1268    }
1269}