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 = "Vec::is_empty")]
88 pub procedures: Vec<ProcedureDescriptor>,
89 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
90 pub extensions: BTreeMap<String, serde_json::Value>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct TableDescriptor {
95 pub catalog_id: String,
96 pub name: String,
97 pub schema_generation: u64,
98 pub fingerprint: String,
99 pub created_at: String,
100 pub updated_at: String,
101 pub columns: Vec<ColumnDescriptor>,
102 pub constraints: Vec<ConstraintDescriptor>,
103 pub indexes: Vec<IndexDescriptor>,
104 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
105 pub extensions: BTreeMap<String, serde_json::Value>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct ColumnDescriptor {
110 pub ordinal: u32,
111 pub name: String,
112 pub data_type: DataTypeDescriptor,
113 pub nullable: bool,
114 pub auto_increment: bool,
115 pub default_expression: Option<String>,
116 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
117 pub extensions: BTreeMap<String, serde_json::Value>,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(tag = "type", rename_all = "snake_case")]
122pub enum DataTypeDescriptor {
123 Null,
124 Integer,
125 Float,
126 DoublePrecision,
127 Text {
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 max_chars: Option<u32>,
130 },
131 Boolean,
132 Timestamp,
134 CivilTimestamp,
136 Time,
138 Date,
139 Json,
140 Uuid,
141 Bytes,
142 Decimal {
143 precision: Option<u8>,
144 scale: Option<u8>,
145 },
146 Vector {
147 dimensions: u16,
148 },
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct ConstraintDescriptor {
153 pub id: u64,
154 pub name: String,
155 #[serde(flatten)]
156 pub definition: ConstraintDefinition,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(tag = "constraint_type", rename_all = "snake_case")]
161pub enum ConstraintDefinition {
162 PrimaryKey {
163 columns: Vec<String>,
164 },
165 Unique {
166 columns: Vec<String>,
167 owned_index: String,
168 },
169 ForeignKey {
170 columns: Vec<String>,
171 referenced_table: String,
172 referenced_columns: Vec<String>,
173 on_delete: ForeignKeyActionDescriptor,
174 on_update: ForeignKeyActionDescriptor,
175 },
176 Check {
177 column: Option<String>,
178 expression: String,
179 ordinal: u32,
180 },
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "snake_case")]
185pub enum ForeignKeyActionDescriptor {
186 Restrict,
187 Cascade,
188 SetNull,
189 NoAction,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub struct IndexDescriptor {
194 pub name: String,
195 pub method: String,
196 pub columns: Vec<String>,
197 pub unique: bool,
198 pub predicate: Option<String>,
199 pub options: BTreeMap<String, serde_json::Value>,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct ViewDescriptor {
204 pub name: String,
205 pub query: String,
206 pub dependencies: Vec<String>,
207 pub result_columns: Vec<ResultColumnDescriptor>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211pub struct ResultColumnDescriptor {
212 pub name: String,
213 pub data_type: DataTypeDescriptor,
214 pub nullable: bool,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218pub struct ProcedureDescriptor {
219 pub catalog_id: String,
220 pub name: String,
221 pub definition_revision: u64,
222 pub fingerprint: String,
223 pub source_sha256: String,
224 pub language: String,
225 pub language_version: u16,
226 pub compiler_abi: u32,
227 pub runtime_abi: u32,
228 pub security: RoutineSecurityDescriptor,
229 pub volatility: RoutineVolatilityDescriptor,
230 pub arguments: Vec<RoutineArgumentDescriptor>,
231 pub result: RoutineResultDescriptor,
232 pub resource_policy: RoutineResourcePolicyDescriptor,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236pub struct RoutineArgumentDescriptor {
237 pub ordinal: u32,
238 pub name: String,
239 pub mode: RoutineArgumentModeDescriptor,
240 pub sql_type: String,
241 pub data_type: Option<DataTypeDescriptor>,
245 pub nullable: bool,
246 pub default_expression: Option<String>,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "snake_case")]
251pub enum RoutineArgumentModeDescriptor {
252 In,
253 Out,
254 InOut,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(tag = "kind", rename_all = "snake_case")]
259pub enum RoutineResultDescriptor {
260 Void,
261 Scalar {
262 sql_type: String,
263 data_type: Option<DataTypeDescriptor>,
264 nullable: bool,
265 },
266 Table {
267 columns: Vec<RoutineResultColumnDescriptor>,
268 },
269 Trigger,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct RoutineResultColumnDescriptor {
274 pub ordinal: u32,
275 pub name: String,
276 pub sql_type: String,
277 pub data_type: Option<DataTypeDescriptor>,
278 pub nullable: bool,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(rename_all = "snake_case")]
283pub enum RoutineSecurityDescriptor {
284 Invoker,
285 Definer,
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum RoutineVolatilityDescriptor {
291 Immutable,
292 Stable,
293 Volatile,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
297pub struct RoutineResourcePolicyDescriptor {
298 pub instructions: u64,
299 pub heap_bytes: u64,
300 pub frames: u32,
301 pub sql_statements: u64,
302 pub rows: u64,
303 pub result_bytes: u64,
304 pub deadline_ms: u64,
305}
306
307impl TableDescriptor {
308 pub fn to_json(&self) -> Result<String, DescriptorError> {
309 DescriptorEnvelope::new(DescriptorKind::Table, self.clone()).to_json()
310 }
311
312 pub fn from_json(json: &str) -> Result<Self, DescriptorError> {
313 Ok(DescriptorEnvelope::<Self>::from_json(json, DescriptorKind::Table)?.payload)
314 }
315
316 pub fn form_descriptor(&self) -> crate::TableFormDescriptor {
317 crate::TableFormDescriptor::from_table(self)
318 }
319
320 pub fn computed_fingerprint(&self) -> Result<String, DescriptorError> {
322 let mut canonical = self.clone();
323 canonical.fingerprint.clear();
324 canonical_fingerprint(&canonical)
325 }
326
327 pub fn refresh_fingerprint(&mut self) -> Result<(), DescriptorError> {
328 self.fingerprint = self.computed_fingerprint()?;
329 Ok(())
330 }
331}
332
333impl DatabaseDescriptor {
334 pub fn to_json(&self) -> Result<String, DescriptorError> {
335 DescriptorEnvelope::new(DescriptorKind::Database, self.clone()).to_json()
336 }
337
338 pub fn from_json(json: &str) -> Result<Self, DescriptorError> {
339 Ok(DescriptorEnvelope::<Self>::from_json(json, DescriptorKind::Database)?.payload)
340 }
341
342 pub fn computed_fingerprint(&self) -> Result<String, DescriptorError> {
345 let mut canonical = self.clone();
346 canonical.fingerprint.clear();
347 canonical_fingerprint(&canonical)
348 }
349
350 pub fn refresh_fingerprint(&mut self) -> Result<(), DescriptorError> {
351 self.fingerprint = self.computed_fingerprint()?;
352 Ok(())
353 }
354}
355
356impl ProcedureDescriptor {
357 pub fn computed_fingerprint(&self) -> Result<String, DescriptorError> {
360 let mut canonical = self.clone();
361 canonical.fingerprint.clear();
362 canonical_fingerprint(&canonical)
363 }
364
365 pub fn refresh_fingerprint(&mut self) -> Result<(), DescriptorError> {
366 self.fingerprint = self.computed_fingerprint()?;
367 Ok(())
368 }
369}
370
371pub fn canonical_fingerprint<T: Serialize>(value: &T) -> Result<String, DescriptorError> {
372 let encoded = serde_json::to_vec(value)?;
373 let digest = Sha256::digest(encoded);
374 let mut fingerprint = String::with_capacity(digest.len() * 2);
375 for byte in digest {
376 write!(&mut fingerprint, "{byte:02x}").expect("writing to String cannot fail");
377 }
378 Ok(fingerprint)
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn descriptor_envelope_round_trip_is_versioned_and_canonical() {
387 let descriptor = TableDescriptor {
388 catalog_id: "018f2b34-7a10-7cc2-8f3a-9d4b5c6d7e01".to_string(),
389 name: "people".to_string(),
390 schema_generation: 7,
391 fingerprint: "abc".to_string(),
392 created_at: "2026-08-21T00:00:00Z".to_string(),
393 updated_at: "2026-08-21T00:00:00Z".to_string(),
394 columns: Vec::new(),
395 constraints: Vec::new(),
396 indexes: Vec::new(),
397 extensions: BTreeMap::new(),
398 };
399 let envelope = DescriptorEnvelope::new(DescriptorKind::Table, descriptor.clone());
400 let json = envelope.to_json().unwrap();
401 assert_eq!(envelope.to_json().unwrap(), json);
402 let decoded =
403 DescriptorEnvelope::<TableDescriptor>::from_json(&json, DescriptorKind::Table).unwrap();
404 assert_eq!(decoded.payload, descriptor);
405 assert_eq!(
406 TableDescriptor::from_json(&descriptor.to_json().unwrap()).unwrap(),
407 descriptor
408 );
409 assert!(
410 DescriptorEnvelope::<TableDescriptor>::from_json(&json, DescriptorKind::Database)
411 .is_err()
412 );
413 }
414
415 #[test]
416 fn text_descriptor_preserves_old_unbounded_json_and_bounded_identity() {
417 let unbounded = DataTypeDescriptor::Text { max_chars: None };
418 assert_eq!(
419 serde_json::to_string(&unbounded).unwrap(),
420 r#"{"type":"text"}"#
421 );
422 assert_eq!(
423 serde_json::from_str::<DataTypeDescriptor>(r#"{"type":"text"}"#).unwrap(),
424 unbounded
425 );
426
427 let bounded = DataTypeDescriptor::Text {
428 max_chars: Some(255),
429 };
430 assert_eq!(
431 serde_json::to_string(&bounded).unwrap(),
432 r#"{"type":"text","max_chars":255}"#
433 );
434 assert_ne!(
435 canonical_fingerprint(&unbounded).unwrap(),
436 canonical_fingerprint(&bounded).unwrap()
437 );
438 }
439
440 #[test]
441 fn double_precision_has_a_distinct_stable_descriptor_identity() {
442 let double = DataTypeDescriptor::DoublePrecision;
443 assert_eq!(
444 serde_json::to_string(&double).unwrap(),
445 r#"{"type":"double_precision"}"#
446 );
447 assert_ne!(
448 canonical_fingerprint(&DataTypeDescriptor::Float).unwrap(),
449 canonical_fingerprint(&double).unwrap()
450 );
451 }
452}