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