Skip to main content

radixdb_core/
schema_descriptor.rs

1use std::collections::BTreeMap;
2use std::fmt::Write;
3
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6
7pub const SCHEMA_DESCRIPTOR_VERSION: &str = "radixdb.schema.v1";
8
9#[derive(Debug, thiserror::Error)]
10pub enum DescriptorError {
11    #[error("unsupported schema descriptor version '{0}'")]
12    UnsupportedVersion(String),
13    #[error("schema descriptor kind mismatch: expected {expected:?}, got {actual:?}")]
14    KindMismatch {
15        expected: DescriptorKind,
16        actual: DescriptorKind,
17    },
18    #[error("schema descriptor JSON error: {0}")]
19    Json(#[from] serde_json::Error),
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum DescriptorKind {
25    Table,
26    Database,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct DescriptorEnvelope<T> {
31    pub descriptor: String,
32    pub kind: DescriptorKind,
33    pub payload: T,
34}
35
36impl<T> DescriptorEnvelope<T>
37where
38    T: Serialize,
39{
40    pub fn new(kind: DescriptorKind, payload: T) -> Self {
41        Self {
42            descriptor: SCHEMA_DESCRIPTOR_VERSION.to_string(),
43            kind,
44            payload,
45        }
46    }
47
48    pub fn to_json(&self) -> Result<String, DescriptorError> {
49        Ok(serde_json::to_string(self)?)
50    }
51
52    pub fn to_pretty_json(&self) -> Result<String, DescriptorError> {
53        Ok(serde_json::to_string_pretty(self)?)
54    }
55}
56
57impl<T> DescriptorEnvelope<T>
58where
59    T: for<'de> Deserialize<'de>,
60{
61    pub fn from_json(json: &str, expected: DescriptorKind) -> Result<Self, DescriptorError> {
62        let envelope: Self = serde_json::from_str(json)?;
63        if envelope.descriptor != SCHEMA_DESCRIPTOR_VERSION {
64            return Err(DescriptorError::UnsupportedVersion(envelope.descriptor));
65        }
66        if envelope.kind != expected {
67            return Err(DescriptorError::KindMismatch {
68                expected,
69                actual: envelope.kind,
70            });
71        }
72        Ok(envelope)
73    }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct DatabaseDescriptor {
78    pub schema_generation: u64,
79    pub fingerprint: String,
80    pub tables: Vec<TableDescriptor>,
81    pub views: Vec<ViewDescriptor>,
82    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
83    pub extensions: BTreeMap<String, serde_json::Value>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct TableDescriptor {
88    pub catalog_id: String,
89    pub name: String,
90    pub schema_generation: u64,
91    pub fingerprint: String,
92    pub created_at: String,
93    pub updated_at: String,
94    pub columns: Vec<ColumnDescriptor>,
95    pub constraints: Vec<ConstraintDescriptor>,
96    pub indexes: Vec<IndexDescriptor>,
97    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
98    pub extensions: BTreeMap<String, serde_json::Value>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct ColumnDescriptor {
103    pub ordinal: u32,
104    pub name: String,
105    pub data_type: DataTypeDescriptor,
106    pub nullable: bool,
107    pub auto_increment: bool,
108    pub default_expression: Option<String>,
109    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
110    pub extensions: BTreeMap<String, serde_json::Value>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(tag = "type", rename_all = "snake_case")]
115pub enum DataTypeDescriptor {
116    Null,
117    Integer,
118    Float,
119    Text,
120    Boolean,
121    Timestamp,
122    Date,
123    Json,
124    Uuid,
125    Bytes,
126    Decimal {
127        precision: Option<u8>,
128        scale: Option<u8>,
129    },
130    Vector {
131        dimensions: u16,
132    },
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub struct ConstraintDescriptor {
137    pub id: u64,
138    pub name: String,
139    #[serde(flatten)]
140    pub definition: ConstraintDefinition,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(tag = "constraint_type", rename_all = "snake_case")]
145pub enum ConstraintDefinition {
146    PrimaryKey {
147        columns: Vec<String>,
148    },
149    Unique {
150        columns: Vec<String>,
151        owned_index: String,
152    },
153    ForeignKey {
154        columns: Vec<String>,
155        referenced_table: String,
156        referenced_columns: Vec<String>,
157        on_delete: ForeignKeyActionDescriptor,
158        on_update: ForeignKeyActionDescriptor,
159    },
160    Check {
161        column: Option<String>,
162        expression: String,
163        ordinal: u32,
164    },
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "snake_case")]
169pub enum ForeignKeyActionDescriptor {
170    Restrict,
171    Cascade,
172    SetNull,
173    NoAction,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177pub struct IndexDescriptor {
178    pub name: String,
179    pub method: String,
180    pub columns: Vec<String>,
181    pub unique: bool,
182    pub predicate: Option<String>,
183    pub options: BTreeMap<String, serde_json::Value>,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct ViewDescriptor {
188    pub name: String,
189    pub query: String,
190    pub dependencies: Vec<String>,
191    pub result_columns: Vec<ResultColumnDescriptor>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct ResultColumnDescriptor {
196    pub name: String,
197    pub data_type: DataTypeDescriptor,
198    pub nullable: bool,
199}
200
201impl TableDescriptor {
202    pub fn to_json(&self) -> Result<String, DescriptorError> {
203        DescriptorEnvelope::new(DescriptorKind::Table, self.clone()).to_json()
204    }
205
206    pub fn from_json(json: &str) -> Result<Self, DescriptorError> {
207        Ok(DescriptorEnvelope::<Self>::from_json(json, DescriptorKind::Table)?.payload)
208    }
209
210    pub fn form_descriptor(&self) -> crate::TableFormDescriptor {
211        crate::TableFormDescriptor::from_table(self)
212    }
213
214    /// Compute the schema fingerprint with the fingerprint field itself blank.
215    pub fn computed_fingerprint(&self) -> Result<String, DescriptorError> {
216        let mut canonical = self.clone();
217        canonical.fingerprint.clear();
218        canonical_fingerprint(&canonical)
219    }
220
221    pub fn refresh_fingerprint(&mut self) -> Result<(), DescriptorError> {
222        self.fingerprint = self.computed_fingerprint()?;
223        Ok(())
224    }
225}
226
227impl DatabaseDescriptor {
228    pub fn to_json(&self) -> Result<String, DescriptorError> {
229        DescriptorEnvelope::new(DescriptorKind::Database, self.clone()).to_json()
230    }
231
232    pub fn from_json(json: &str) -> Result<Self, DescriptorError> {
233        Ok(DescriptorEnvelope::<Self>::from_json(json, DescriptorKind::Database)?.payload)
234    }
235
236    /// Compute the database fingerprint from its ordered, already-fingerprinted
237    /// table/view catalog while excluding the fingerprint field itself.
238    pub fn computed_fingerprint(&self) -> Result<String, DescriptorError> {
239        let mut canonical = self.clone();
240        canonical.fingerprint.clear();
241        canonical_fingerprint(&canonical)
242    }
243
244    pub fn refresh_fingerprint(&mut self) -> Result<(), DescriptorError> {
245        self.fingerprint = self.computed_fingerprint()?;
246        Ok(())
247    }
248}
249
250pub fn canonical_fingerprint<T: Serialize>(value: &T) -> Result<String, DescriptorError> {
251    let encoded = serde_json::to_vec(value)?;
252    let digest = Sha256::digest(encoded);
253    let mut fingerprint = String::with_capacity(digest.len() * 2);
254    for byte in digest {
255        write!(&mut fingerprint, "{byte:02x}").expect("writing to String cannot fail");
256    }
257    Ok(fingerprint)
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn descriptor_envelope_round_trip_is_versioned_and_canonical() {
266        let descriptor = TableDescriptor {
267            catalog_id: "018f2b34-7a10-7cc2-8f3a-9d4b5c6d7e01".to_string(),
268            name: "people".to_string(),
269            schema_generation: 7,
270            fingerprint: "abc".to_string(),
271            created_at: "2026-08-21T00:00:00Z".to_string(),
272            updated_at: "2026-08-21T00:00:00Z".to_string(),
273            columns: Vec::new(),
274            constraints: Vec::new(),
275            indexes: Vec::new(),
276            extensions: BTreeMap::new(),
277        };
278        let envelope = DescriptorEnvelope::new(DescriptorKind::Table, descriptor.clone());
279        let json = envelope.to_json().unwrap();
280        assert_eq!(envelope.to_json().unwrap(), json);
281        let decoded =
282            DescriptorEnvelope::<TableDescriptor>::from_json(&json, DescriptorKind::Table).unwrap();
283        assert_eq!(decoded.payload, descriptor);
284        assert_eq!(
285            TableDescriptor::from_json(&descriptor.to_json().unwrap()).unwrap(),
286            descriptor
287        );
288        assert!(
289            DescriptorEnvelope::<TableDescriptor>::from_json(&json, DescriptorKind::Database)
290                .is_err()
291        );
292    }
293}