1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{DataTypeDescriptor, ResultColumnDescriptor};
6
7pub const ORM_IR_VERSION: &str = "radixdb.orm.v1";
8
9#[derive(Debug, thiserror::Error)]
10pub enum IrError {
11 #[error("unsupported ORM IR version '{0}'")]
12 UnsupportedVersion(String),
13 #[error("ORM IR kind mismatch: envelope is {envelope:?}, operation is {operation:?}")]
14 KindMismatch { envelope: IrKind, operation: IrKind },
15 #[error("ORM IR JSON error: {0}")]
16 Json(#[from] serde_json::Error),
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum IrKind {
22 Catalog,
23 Ddl,
24 Select,
25 Insert,
26 Upsert,
27 Update,
28 Delete,
29 Explain,
30 Transaction,
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct IrDocument {
35 pub ir: String,
36 pub kind: IrKind,
37 pub payload: Operation,
38}
39
40impl IrDocument {
41 pub fn new(payload: Operation) -> Self {
42 Self {
43 ir: ORM_IR_VERSION.to_string(),
44 kind: payload.kind(),
45 payload,
46 }
47 }
48
49 pub fn validate(&self) -> Result<(), IrError> {
50 if self.ir != ORM_IR_VERSION {
51 return Err(IrError::UnsupportedVersion(self.ir.clone()));
52 }
53 let operation = self.payload.kind();
54 if self.kind != operation {
55 return Err(IrError::KindMismatch {
56 envelope: self.kind,
57 operation,
58 });
59 }
60 Ok(())
61 }
62
63 pub fn to_json(&self) -> Result<String, IrError> {
64 self.validate()?;
65 Ok(serde_json::to_string(self)?)
66 }
67
68 pub fn to_pretty_json(&self) -> Result<String, IrError> {
69 self.validate()?;
70 Ok(serde_json::to_string_pretty(self)?)
71 }
72
73 pub fn from_json(json: &str) -> Result<Self, IrError> {
74 let document: Self = serde_json::from_str(json)?;
75 document.validate()?;
76 Ok(document)
77 }
78
79 pub fn to_redacted_json(&self) -> Result<String, IrError> {
82 self.validate()?;
83 let mut value = serde_json::to_value(self)?;
84 redact_typed_values(&mut value);
85 Ok(serde_json::to_string(&value)?)
86 }
87}
88
89fn redact_typed_values(value: &mut serde_json::Value) {
90 match value {
91 serde_json::Value::Array(values) => {
92 for value in values {
93 redact_typed_values(value);
94 }
95 }
96 serde_json::Value::Object(object) => {
97 let is_typed_value = object
98 .get("type")
99 .and_then(serde_json::Value::as_str)
100 .is_some_and(|tag| {
101 matches!(
102 tag,
103 "integer"
104 | "float"
105 | "text"
106 | "boolean"
107 | "timestamp"
108 | "date"
109 | "json"
110 | "uuid"
111 | "bytes"
112 | "decimal"
113 | "vector"
114 )
115 });
116 if is_typed_value && object.contains_key("value") {
117 object.insert(
118 "value".to_string(),
119 serde_json::Value::String("<redacted>".to_string()),
120 );
121 return;
122 }
123 for value in object.values_mut() {
124 redact_typed_values(value);
125 }
126 }
127 _ => {}
128 }
129}
130
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132#[serde(tag = "node", rename_all = "snake_case")]
133#[allow(clippy::large_enum_variant)]
137pub enum Operation {
138 Catalog { operation: CatalogOperation },
139 Ddl { operation: DdlOperation },
140 Select { query: Select },
141 Insert { statement: Insert },
142 Upsert { statement: Upsert },
143 Update { statement: Update },
144 Delete { statement: Delete },
145 Explain { statement: Explain },
146 Transaction { statement: TransactionOperation },
147}
148
149impl Operation {
150 pub fn kind(&self) -> IrKind {
151 match self {
152 Self::Catalog { .. } => IrKind::Catalog,
153 Self::Ddl { .. } => IrKind::Ddl,
154 Self::Select { .. } => IrKind::Select,
155 Self::Insert { .. } => IrKind::Insert,
156 Self::Upsert { .. } => IrKind::Upsert,
157 Self::Update { .. } => IrKind::Update,
158 Self::Delete { .. } => IrKind::Delete,
159 Self::Explain { .. } => IrKind::Explain,
160 Self::Transaction { .. } => IrKind::Transaction,
161 }
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166#[serde(tag = "operation", rename_all = "snake_case")]
167pub enum CatalogOperation {
168 ListTables,
169 DescribeTable { table: String },
170 DescribeDatabase,
171 ShowIndexes { table: String },
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175#[serde(tag = "type", content = "value", rename_all = "snake_case")]
176pub enum TypedValue {
177 Null(DataTypeDescriptor),
178 Integer(i64),
179 Float(FloatValue),
180 Text(String),
181 Boolean(bool),
182 Timestamp(String),
184 CivilTimestamp(String),
186 Time(String),
188 Date(String),
189 Json(serde_json::Value),
190 Uuid(String),
191 Bytes(String),
192 Decimal(String),
193 Vector(Vec<f32>),
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
197#[serde(untagged)]
198pub enum FloatValue {
199 Number(f64),
200 Special(FloatSpecial),
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(rename_all = "snake_case")]
205pub enum FloatSpecial {
206 Nan,
207 PositiveInfinity,
208 NegativeInfinity,
209}
210
211impl From<f64> for FloatValue {
212 fn from(value: f64) -> Self {
213 if value.is_nan() {
214 Self::Special(FloatSpecial::Nan)
215 } else if value == f64::INFINITY {
216 Self::Special(FloatSpecial::PositiveInfinity)
217 } else if value == f64::NEG_INFINITY {
218 Self::Special(FloatSpecial::NegativeInfinity)
219 } else {
220 Self::Number(value)
221 }
222 }
223}
224
225impl FloatValue {
226 pub fn as_f64(self) -> f64 {
227 match self {
228 Self::Number(value) => value,
229 Self::Special(FloatSpecial::Nan) => f64::NAN,
230 Self::Special(FloatSpecial::PositiveInfinity) => f64::INFINITY,
231 Self::Special(FloatSpecial::NegativeInfinity) => f64::NEG_INFINITY,
232 }
233 }
234}
235
236impl TypedValue {
237 pub fn data_type(&self) -> DataTypeDescriptor {
238 match self {
239 Self::Null(data_type) => data_type.clone(),
240 Self::Integer(_) => DataTypeDescriptor::Integer,
241 Self::Float(_) => DataTypeDescriptor::Float,
242 Self::Text(_) => DataTypeDescriptor::Text { max_chars: None },
243 Self::Boolean(_) => DataTypeDescriptor::Boolean,
244 Self::Timestamp(_) => DataTypeDescriptor::Timestamp,
245 Self::CivilTimestamp(_) => DataTypeDescriptor::CivilTimestamp,
246 Self::Time(_) => DataTypeDescriptor::Time,
247 Self::Date(_) => DataTypeDescriptor::Date,
248 Self::Json(_) => DataTypeDescriptor::Json,
249 Self::Uuid(_) => DataTypeDescriptor::Uuid,
250 Self::Bytes(_) => DataTypeDescriptor::Bytes,
251 Self::Decimal(_) => DataTypeDescriptor::Decimal {
252 precision: None,
253 scale: None,
254 },
255 Self::Vector(values) => DataTypeDescriptor::Vector {
256 dimensions: u16::try_from(values.len()).unwrap_or(u16::MAX),
257 },
258 }
259 }
260
261 pub fn decimal_parts(&self) -> Result<(i128, u8, u8), TypedValueError> {
265 match self {
266 Self::Decimal(value) => parse_decimal_literal(value),
267 _ => Err(TypedValueError::ExpectedDecimal),
268 }
269 }
270}
271
272#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
273pub enum TypedValueError {
274 #[error("expected a DECIMAL typed value")]
275 ExpectedDecimal,
276 #[error("invalid DECIMAL literal '{0}'")]
277 InvalidDecimal(String),
278 #[error("DECIMAL precision must be in 1..=38")]
279 DecimalPrecision,
280 #[error("DECIMAL scale must not exceed precision")]
281 DecimalScale,
282 #[error("DECIMAL coefficient exceeds declared precision")]
283 DecimalCoefficient,
284}
285
286pub fn parse_decimal_literal(value: &str) -> Result<(i128, u8, u8), TypedValueError> {
287 let value = value.trim();
288 let (negative, unsigned) = match value.as_bytes().first() {
289 Some(b'-') => (true, &value[1..]),
290 Some(b'+') => (false, &value[1..]),
291 _ => (false, value),
292 };
293 let mut parts = unsigned.split('.');
294 let integer = parts.next().unwrap_or_default();
295 let fractional = parts.next().unwrap_or_default();
296 if parts.next().is_some()
297 || integer.is_empty()
298 || !integer.bytes().all(|byte| byte.is_ascii_digit())
299 || !fractional.bytes().all(|byte| byte.is_ascii_digit())
300 {
301 return Err(TypedValueError::InvalidDecimal(value.to_string()));
302 }
303
304 let digits = format!("{integer}{fractional}");
305 let precision = u8::try_from(digits.len()).map_err(|_| TypedValueError::DecimalPrecision)?;
306 let scale = u8::try_from(fractional.len()).map_err(|_| TypedValueError::DecimalPrecision)?;
307 if precision == 0 || precision > 38 {
308 return Err(TypedValueError::DecimalPrecision);
309 }
310 if scale > precision {
311 return Err(TypedValueError::DecimalScale);
312 }
313
314 let mut unscaled = digits
315 .parse::<i128>()
316 .map_err(|_| TypedValueError::DecimalCoefficient)?;
317 if negative {
318 unscaled = unscaled
319 .checked_neg()
320 .ok_or(TypedValueError::DecimalCoefficient)?;
321 }
322 let coefficient_digits = if unscaled == 0 {
323 1
324 } else {
325 unscaled.unsigned_abs().ilog10() as usize + 1
326 };
327 if coefficient_digits > usize::from(precision) {
328 return Err(TypedValueError::DecimalCoefficient);
329 }
330 Ok((unscaled, precision, scale))
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
334pub struct ColumnRef {
335 pub relation: Option<String>,
336 pub name: String,
337}
338
339impl ColumnRef {
340 pub fn new(name: impl Into<String>) -> Self {
341 Self {
342 relation: None,
343 name: name.into(),
344 }
345 }
346
347 pub fn qualified(relation: impl Into<String>, name: impl Into<String>) -> Self {
348 Self {
349 relation: Some(relation.into()),
350 name: name.into(),
351 }
352 }
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
356#[serde(rename_all = "snake_case")]
357pub enum UnaryOperator {
358 Not,
359 Negate,
360 Positive,
361}
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
364#[serde(rename_all = "snake_case")]
365pub enum BinaryOperator {
366 Eq,
367 Ne,
368 Lt,
369 Lte,
370 Gt,
371 Gte,
372 And,
373 Or,
374 Xor,
375 Add,
376 Subtract,
377 Multiply,
378 Divide,
379 Modulo,
380 Like,
381 NotLike,
382 Glob,
383 Regexp,
384 IsDistinctFrom,
385 IsNotDistinctFrom,
386}
387
388#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
389#[serde(tag = "node", rename_all = "snake_case")]
390pub enum Expression {
391 Column {
392 column: ColumnRef,
393 },
394 Literal {
395 value: TypedValue,
396 },
397 Star {
398 relation: Option<String>,
399 },
400 Unary {
401 operator: UnaryOperator,
402 expression: Box<Expression>,
403 },
404 Binary {
405 left: Box<Expression>,
406 operator: BinaryOperator,
407 right: Box<Expression>,
408 },
409 Function {
410 name: String,
411 arguments: Vec<Expression>,
412 },
413 Aggregate {
414 name: String,
415 arguments: Vec<Expression>,
416 distinct: bool,
417 filter: Option<Box<Expression>>,
418 order_by: Vec<OrderBy>,
419 },
420 Window {
421 function: Box<Expression>,
422 specification: WindowSpecification,
423 },
424 Cast {
425 expression: Box<Expression>,
426 data_type: DataTypeDescriptor,
427 },
428 Case {
429 operand: Option<Box<Expression>>,
430 branches: Vec<CaseBranch>,
431 otherwise: Option<Box<Expression>>,
432 },
433 IsNull {
434 expression: Box<Expression>,
435 negated: bool,
436 },
437 Between {
438 expression: Box<Expression>,
439 lower: Box<Expression>,
440 upper: Box<Expression>,
441 negated: bool,
442 },
443 InList {
444 expression: Box<Expression>,
445 values: Vec<Expression>,
446 negated: bool,
447 },
448 InSubquery {
449 expression: Box<Expression>,
450 query: Box<Select>,
451 negated: bool,
452 },
453 Exists {
454 query: Box<Select>,
455 negated: bool,
456 },
457 ScalarSubquery {
458 query: Box<Select>,
459 },
460 Tuple {
461 values: Vec<Expression>,
462 },
463 Navigation {
466 root: String,
467 path: Vec<String>,
468 },
469 Grouping {
470 expressions: Vec<Expression>,
471 },
472}
473
474impl Expression {
475 pub fn column(name: impl Into<String>) -> Self {
476 Self::Column {
477 column: ColumnRef::new(name),
478 }
479 }
480
481 pub fn literal(value: TypedValue) -> Self {
482 Self::Literal { value }
483 }
484}
485
486#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
487pub struct CaseBranch {
488 pub when: Expression,
489 pub then: Expression,
490}
491
492#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
493#[serde(rename_all = "snake_case")]
494pub enum SortDirection {
495 Asc,
496 Desc,
497}
498
499#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
500#[serde(rename_all = "snake_case")]
501pub enum NullPlacement {
502 First,
503 Last,
504}
505
506#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
507pub struct OrderBy {
508 pub expression: Expression,
509 pub direction: SortDirection,
510 pub nulls: Option<NullPlacement>,
511}
512
513#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
514#[serde(rename_all = "snake_case")]
515pub enum WindowFrameUnit {
516 Rows,
517 Range,
518}
519
520#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
521#[serde(tag = "bound", content = "offset", rename_all = "snake_case")]
522pub enum WindowFrameBound {
523 UnboundedPreceding,
524 Preceding(u64),
525 CurrentRow,
526 Following(u64),
527 UnboundedFollowing,
528}
529
530#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
531pub struct WindowFrame {
532 pub unit: WindowFrameUnit,
533 pub start: WindowFrameBound,
534 pub end: Option<WindowFrameBound>,
535}
536
537#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
538pub struct WindowSpecification {
539 pub name: Option<String>,
540 pub partition_by: Vec<Expression>,
541 pub order_by: Vec<OrderBy>,
542 pub frame: Option<WindowFrame>,
543}
544
545#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
546pub struct NamedWindow {
547 pub name: String,
548 pub specification: WindowSpecification,
549}
550
551#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
552pub struct Projection {
553 pub expression: Expression,
554 pub alias: Option<String>,
555}
556
557#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
558#[serde(rename_all = "snake_case")]
559pub enum JoinKind {
560 Inner,
561 Left,
562 Right,
563 Full,
564 Cross,
565}
566
567#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
568#[serde(tag = "node", rename_all = "snake_case")]
569pub enum Relation {
570 Table {
571 name: String,
572 alias: Option<String>,
573 },
574 Cte {
575 name: String,
576 alias: Option<String>,
577 },
578 Derived {
579 query: Box<Select>,
580 alias: String,
581 },
582 Values {
583 rows: Vec<Vec<Expression>>,
584 alias: String,
585 columns: Vec<String>,
586 },
587 Join {
588 left: Box<Relation>,
589 right: Box<Relation>,
590 kind: JoinKind,
591 on: Option<Expression>,
592 },
593}
594
595#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
596#[serde(tag = "grouping", rename_all = "snake_case")]
597pub enum Grouping {
598 Expressions { expressions: Vec<Expression> },
599 Rollup { expressions: Vec<Expression> },
600 Cube { expressions: Vec<Expression> },
601 Sets { sets: Vec<Vec<Expression>> },
602}
603
604#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
605pub struct CommonTableExpression {
606 pub name: String,
607 pub columns: Vec<String>,
608 pub query: Box<Select>,
609}
610
611#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
612#[serde(rename_all = "snake_case")]
613pub enum SetOperator {
614 Union,
615 UnionAll,
616 Intersect,
617 Except,
618}
619
620#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
621pub struct SetArm {
622 pub operator: SetOperator,
623 pub query: Box<Select>,
624}
625
626#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
627pub struct Select {
628 pub ctes: Vec<CommonTableExpression>,
629 pub recursive: bool,
630 pub distinct: bool,
631 pub distinct_on: Vec<Expression>,
632 pub projection: Vec<Projection>,
633 pub from: Option<Relation>,
634 pub filter: Option<Expression>,
635 pub group_by: Option<Grouping>,
636 pub having: Option<Expression>,
637 pub windows: Vec<NamedWindow>,
638 pub set_operations: Vec<SetArm>,
639 pub order_by: Vec<OrderBy>,
640 pub limit: Option<u64>,
641 pub offset: Option<u64>,
642 pub expected_result_shape: Vec<ResultColumnDescriptor>,
643}
644
645#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
646pub struct Assignment {
647 pub column: String,
648 pub value: Expression,
649}
650
651#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
652pub struct Insert {
653 pub table: String,
654 pub columns: Vec<String>,
655 pub rows: Vec<Vec<Expression>>,
656 pub source: Option<Box<Select>>,
657 pub returning: Vec<Projection>,
658}
659
660#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
661pub struct Upsert {
662 pub insert: Insert,
663 pub conflict_columns: Vec<String>,
664 pub assignments: Vec<Assignment>,
665}
666
667#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
668pub struct Update {
669 pub table: String,
670 pub alias: Option<String>,
671 pub assignments: Vec<Assignment>,
672 pub from: Option<Relation>,
673 pub filter: Option<Expression>,
674 pub returning: Vec<Projection>,
675}
676
677#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
678pub struct Delete {
679 pub table: String,
680 pub alias: Option<String>,
681 pub using: Option<Relation>,
682 pub filter: Option<Expression>,
683 pub all_rows: bool,
684 pub returning: Vec<Projection>,
685}
686
687#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
688pub struct Explain {
689 pub analyze: bool,
690 pub operation: Box<Operation>,
691}
692
693#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
694#[serde(tag = "operation", rename_all = "snake_case")]
695pub enum TransactionOperation {
696 Begin,
697 Commit,
698 Rollback,
699 Savepoint { name: String },
700 RollbackToSavepoint { name: String },
701 ReleaseSavepoint { name: String },
702}
703
704#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
705pub struct ColumnDefinition {
706 pub name: String,
707 pub data_type: DataTypeDescriptor,
708 pub nullable: bool,
709 pub primary_key: bool,
710 pub unique: bool,
711 pub auto_increment: bool,
712 pub default: Option<Expression>,
713 pub check: Option<Expression>,
714 pub reference: Option<ReferenceDefinition>,
715 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
716 pub extensions: BTreeMap<String, serde_json::Value>,
717}
718
719#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
720pub struct ReferenceDefinition {
721 pub table: String,
722 pub column: String,
723 pub on_delete: crate::ForeignKeyActionDescriptor,
724 pub on_update: crate::ForeignKeyActionDescriptor,
725}
726
727#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
728#[serde(tag = "constraint", rename_all = "snake_case")]
729pub enum ConstraintDefinitionIr {
730 PrimaryKey {
731 columns: Vec<String>,
732 },
733 Unique {
734 columns: Vec<String>,
735 },
736 ForeignKey {
737 columns: Vec<String>,
738 referenced_table: String,
739 referenced_columns: Vec<String>,
740 on_delete: crate::ForeignKeyActionDescriptor,
741 on_update: crate::ForeignKeyActionDescriptor,
742 },
743 Check {
744 expression: Expression,
745 },
746}
747
748#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
749pub struct IndexDefinition {
750 pub name: String,
751 pub table: String,
752 pub columns: Vec<String>,
753 pub unique: bool,
754 #[serde(default)]
755 pub if_not_exists: bool,
756 pub method: Option<String>,
757 pub predicate: Option<Expression>,
758 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
759 pub options: BTreeMap<String, TypedValue>,
760}
761
762#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
763#[serde(tag = "operation", rename_all = "snake_case")]
764pub enum AlterTableAction {
765 AddColumn { column: ColumnDefinition },
766 ModifyColumn { column: ColumnDefinition },
767 DropColumn { column: String },
768 RenameColumn { from: String, to: String },
769 RenameTable { to: String },
770 AddConstraint { constraint: ConstraintDefinitionIr },
771 DropConstraint { name: String, if_exists: bool },
772}
773
774#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
775#[serde(tag = "operation", rename_all = "snake_case")]
776pub enum DdlOperation {
777 CreateTable {
778 table: String,
779 if_not_exists: bool,
780 columns: Vec<ColumnDefinition>,
781 constraints: Vec<ConstraintDefinitionIr>,
782 },
783 CreateTableAs {
784 table: String,
785 if_not_exists: bool,
786 query: Box<Select>,
787 },
788 AlterTable {
789 table: String,
790 action: AlterTableAction,
791 },
792 DropTable {
793 table: String,
794 if_exists: bool,
795 },
796 TruncateTable {
797 table: String,
798 },
799 CreateIndex {
800 index: IndexDefinition,
801 },
802 DropIndex {
803 table: String,
804 index: String,
805 if_exists: bool,
806 },
807 AlterIndex {
808 index: String,
809 new_name: String,
810 },
811}
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816
817 #[test]
818 fn document_round_trip_is_versioned_kind_checked_and_redacted() {
819 let document = IrDocument::new(Operation::Select {
820 query: Select {
821 projection: vec![Projection {
822 expression: Expression::literal(TypedValue::Text("secret".to_string())),
823 alias: Some("value".to_string()),
824 }],
825 ..Select::default()
826 },
827 });
828 let json = document.to_json().unwrap();
829 assert_eq!(IrDocument::from_json(&json).unwrap(), document);
830 let redacted = document.to_redacted_json().unwrap();
831 assert!(!redacted.contains("secret"));
832 assert!(redacted.contains("<redacted>"));
833
834 let mut wrong = document.clone();
835 wrong.kind = IrKind::Delete;
836 assert!(wrong.to_json().is_err());
837
838 let unknown = json.replace(ORM_IR_VERSION, "radixdb.orm.v999");
839 assert!(matches!(
840 IrDocument::from_json(&unknown),
841 Err(IrError::UnsupportedVersion(_))
842 ));
843 }
844}