Skip to main content

radixdb_orm/
ir.rs

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    /// Serialize a log-safe form. Typed values keep their public type tag but
80    /// their payload is replaced; executable JSON is never used as log JSON.
81    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// This is the stable, language-neutral JSON root. Boxing selected variants
134// would leak an allocation-driven Rust detail into every constructor without
135// improving the wire representation, so the size trade-off is intentional.
136#[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    /// Decode the language-neutral DECIMAL text form into RadixDB's exact
255    /// coefficient/precision/scale tuple. SDK transports use this one parser
256    /// so embedded and TCP execution cannot disagree about admission.
257    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    /// Read-only RadixDB navigation path. `root` is a table alias and every
457    /// path segment is an authoritative reference/column identifier.
458    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}