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}