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 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 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}