1use std::fmt::Write;
4
5use crate::*;
6
7#[derive(Debug, thiserror::Error)]
8pub enum CodegenError {
9 #[error(transparent)]
10 Descriptor(#[from] DescriptorError),
11 #[error(
12 "descriptor fingerprint mismatch for '{name}': expected {expected}, computed {actual}"
13 )]
14 Fingerprint {
15 name: String,
16 expected: String,
17 actual: String,
18 },
19 #[error("identifier '{0}' cannot be represented safely in generated Rust")]
20 Identifier(String),
21 #[error("reference '{table}.{column}' must use one source and one target column")]
22 UnsupportedReference { table: String, column: String },
23 #[error("reference target table '{0}' is absent from the descriptor")]
24 MissingReferenceTarget(String),
25 #[error("reference source column '{table}.{column}' is absent from the descriptor")]
26 MissingReferenceSourceColumn { table: String, column: String },
27 #[error("reference target column '{table}.{column}' is absent from the descriptor")]
28 MissingReferenceTargetColumn { table: String, column: String },
29 #[error(
30 "reference target '{table}.{column}' must be a one-column PRIMARY KEY or UNIQUE NOT NULL key"
31 )]
32 UnsupportedReferenceTargetKey { table: String, column: String },
33 #[error(
34 "reference type mismatch: '{source_table}.{source_column}' is {source_type:?}, but '{target_table}.{target_column}' is {target_type:?}"
35 )]
36 ReferenceTypeMismatch {
37 source_table: String,
38 source_column: String,
39 source_type: DataTypeDescriptor,
40 target_table: String,
41 target_column: String,
42 target_type: DataTypeDescriptor,
43 },
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct GeneratedRust {
48 pub source: String,
49 pub descriptor_fingerprint: String,
50}
51
52pub fn generate_rust_database(
53 descriptor: &DescriptorEnvelope<DatabaseDescriptor>,
54) -> Result<GeneratedRust, CodegenError> {
55 if descriptor.descriptor != SCHEMA_DESCRIPTOR_VERSION
56 || descriptor.kind != DescriptorKind::Database
57 {
58 return Err(CodegenError::Descriptor(DescriptorError::KindMismatch {
59 expected: DescriptorKind::Database,
60 actual: descriptor.kind,
61 }));
62 }
63 validate_database_fingerprints(&descriptor.payload)?;
64
65 let mut output = String::new();
66 output.push_str("// @generated by radixdb-orm; DO NOT EDIT.\n");
67 output.push_str("// Source: radixdb.schema.v1 descriptor. No live database was accessed.\n\n");
68 output.push_str("#[allow(unused_imports)]\nuse radixdb_orm::{AsyncOrmGeneratedRecordSession, DataTypeDescriptor, DynamicRecord, Expr, FieldState, FieldValue, GeneratedEntity, GeneratedQuery, GeneratedRecord, GeneratedRecordError, InsertBuilder, OrmGeneratedRecordSession, QueryBuilder, Reference, Relation, RecordMutation, TableDescriptor, TypedColumn, TypedKey, TypedValue, UpdateBuilder, DeleteBuilder, decode_generated_field, decode_generated_reference_field, ensure_schema_fingerprint};\n\n");
69 writeln!(
70 output,
71 "pub const DATABASE_SCHEMA_FINGERPRINT: &str = {:?};\n",
72 descriptor.payload.fingerprint
73 )
74 .unwrap();
75
76 for table in &descriptor.payload.tables {
77 render_table(&mut output, table, &descriptor.payload.tables)?;
78 }
79
80 Ok(GeneratedRust {
81 source: output,
82 descriptor_fingerprint: descriptor.payload.fingerprint.clone(),
83 })
84}
85
86fn validate_database_fingerprints(database: &DatabaseDescriptor) -> Result<(), CodegenError> {
87 for table in &database.tables {
88 let actual = table.computed_fingerprint()?;
89 if table.fingerprint != actual {
90 return Err(CodegenError::Fingerprint {
91 name: table.name.clone(),
92 expected: table.fingerprint.clone(),
93 actual,
94 });
95 }
96 }
97 let actual = database.computed_fingerprint()?;
98 if database.fingerprint != actual {
99 return Err(CodegenError::Fingerprint {
100 name: "database".to_string(),
101 expected: database.fingerprint.clone(),
102 actual,
103 });
104 }
105 Ok(())
106}
107
108fn render_table(
109 output: &mut String,
110 table: &TableDescriptor,
111 tables: &[TableDescriptor],
112) -> Result<(), CodegenError> {
113 validate_reference_shapes(table, tables)?;
114 let entity = rust_type_name(&table.name)?;
115 let record = format!("{entity}Record");
116 writeln!(
117 output,
118 "#[derive(Debug, Clone, PartialEq)]\npub struct {record} {{"
119 )
120 .unwrap();
121 for column in &table.columns {
122 let field_type = match column_reference(table, &column.name) {
123 Some((target_table, _)) => rust_type_name(target_table)?,
124 None => rust_type(&column.data_type).to_string(),
125 };
126 writeln!(
127 output,
128 " pub {}: FieldState<{}>,",
129 rust_field_name(&column.name)?,
130 match column_reference(table, &column.name) {
131 Some(_) => format!("Reference<{field_type}>"),
132 None => field_type,
133 }
134 )
135 .unwrap();
136 }
137 output.push_str("}\n\n");
138
139 writeln!(output, "impl Default for {record} {{").unwrap();
140 output.push_str(" fn default() -> Self {\n Self {\n");
141 for column in &table.columns {
142 writeln!(
143 output,
144 " {}: FieldState::typed({}),",
145 rust_field_name(&column.name)?,
146 rust_data_type(&column.data_type)
147 )
148 .unwrap();
149 }
150 output.push_str(" }\n }\n}\n\n");
151
152 writeln!(
153 output,
154 "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub struct {entity};"
155 )
156 .unwrap();
157 writeln!(output, "impl GeneratedEntity for {entity} {{").unwrap();
158 writeln!(output, " type Record = {record};").unwrap();
159 writeln!(output, " const TABLE: &'static str = {:?};", table.name).unwrap();
160 writeln!(
161 output,
162 " const CATALOG_ID: &'static str = {:?};",
163 table.catalog_id
164 )
165 .unwrap();
166 writeln!(
167 output,
168 " const SCHEMA_FINGERPRINT: &'static str = {:?};",
169 table.fingerprint
170 )
171 .unwrap();
172 output.push_str("}\n\n");
173
174 writeln!(output, "impl {entity} {{").unwrap();
175 for column in &table.columns {
176 let column_type = match column_reference(table, &column.name) {
177 Some((target_table, _)) => format!("Reference<{}>", rust_type_name(target_table)?),
178 None => rust_type(&column.data_type).to_string(),
179 };
180 writeln!(
181 output,
182 " pub const {}: TypedColumn<{}, Self> = TypedColumn::new({:?}, {:?}, {}, {});",
183 rust_const_name(&column.name)?,
184 column_type,
185 table.name,
186 column.name,
187 rust_data_type(&column.data_type),
188 column.nullable
189 )
190 .unwrap();
191 }
192 writeln!(
193 output,
194 " #[allow(clippy::new_ret_no_self)]\n pub fn new() -> {record} {{ {record}::default() }}"
195 )
196 .unwrap();
197 writeln!(output, " pub fn table() -> Relation {{ Relation::Table {{ name: {:?}.to_string(), alias: None }} }}", table.name).unwrap();
198 output.push_str(
199 " pub fn query() -> QueryBuilder { QueryBuilder::from_relation(Self::table()) }\n",
200 );
201 writeln!(
202 output,
203 " pub fn insert() -> InsertBuilder {{ InsertBuilder::new({:?}) }}",
204 table.name
205 )
206 .unwrap();
207 writeln!(
208 output,
209 " pub fn upsert() -> InsertBuilder {{ InsertBuilder::new({:?}) }}",
210 table.name
211 )
212 .unwrap();
213 writeln!(
214 output,
215 " pub fn update() -> UpdateBuilder {{ UpdateBuilder::new({:?}) }}",
216 table.name
217 )
218 .unwrap();
219 writeln!(
220 output,
221 " pub fn delete() -> DeleteBuilder {{ DeleteBuilder::new({:?}) }}",
222 table.name
223 )
224 .unwrap();
225 if let Some(primary_key) = primary_reference_key(table) {
226 let column = table
227 .columns
228 .iter()
229 .find(|candidate| candidate.name == primary_key)
230 .expect("constraint column exists");
231 writeln!(output, " pub fn get(key: {}) -> GeneratedQuery<{record}> {{ GeneratedQuery::new(Self::query().select([Expr::star()]).filter(Self::{}.eq(Expr::value({})))) }}", rust_type(&column.data_type), rust_const_name(primary_key)?, owned_typed_value_expression("key", &column.data_type)).unwrap();
232 }
233 for (key, is_primary) in reference_keys(table) {
234 let column = table
235 .columns
236 .iter()
237 .find(|candidate| candidate.name == key)
238 .expect("constraint column exists");
239 let method = if is_primary {
240 "reference".to_string()
241 } else {
242 format!("reference_by_{}", rust_field_name(key)?)
243 };
244 let encoder = format!("encode_{}_key", rust_field_name(key)?);
245 writeln!(
246 output,
247 " fn {encoder}(key: {}) -> TypedValue {{ {} }}",
248 rust_type(&column.data_type),
249 owned_typed_value_expression("key", &column.data_type)
250 )
251 .unwrap();
252 writeln!(
253 output,
254 " pub fn {method}(key: {}) -> Reference<Self> {{ {}Keys::{}.reference(key) }}",
255 rust_type(&column.data_type),
256 entity,
257 rust_const_name(key)?
258 )
259 .unwrap();
260 }
261 output.push_str("}\n\n");
262
263 if !reference_keys(table).is_empty() {
264 writeln!(
265 output,
266 "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub struct {entity}Keys;"
267 )
268 .unwrap();
269 writeln!(output, "impl {entity}Keys {{").unwrap();
270 for (key, is_primary) in reference_keys(table) {
271 let column = table
272 .columns
273 .iter()
274 .find(|candidate| candidate.name == key)
275 .expect("constraint column exists");
276 writeln!(
277 output,
278 " pub const {}: TypedKey<{entity}, {}> = TypedKey::new({entity}::{}, {}, {entity}::encode_{}_key);",
279 rust_const_name(key)?,
280 rust_type(&column.data_type),
281 rust_const_name(key)?,
282 is_primary,
283 rust_field_name(key)?,
284 )
285 .unwrap();
286 }
287 output.push_str("}\n\n");
288 }
289
290 writeln!(output, "impl {record} {{").unwrap();
291 writeln!(output, " pub fn to_dynamic(&self, descriptor: &TableDescriptor) -> Result<DynamicRecord, GeneratedRecordError> {{").unwrap();
292 writeln!(output, " ensure_schema_fingerprint({entity}::SCHEMA_FINGERPRINT, &descriptor.fingerprint)?;").unwrap();
293 output.push_str(" let mut record = DynamicRecord::new(descriptor.clone());\n");
294 for column in &table.columns {
295 let field = rust_field_name(&column.name)?;
296 let typed_value = match column_reference(table, &column.name) {
297 Some(_) => "value.key().clone()".to_string(),
298 None => typed_value_expression("value", &column.data_type),
299 };
300 writeln!(output, " match self.{field}.value() {{").unwrap();
301 output.push_str(" FieldValue::Omitted => {}\n");
302 writeln!(output, " FieldValue::Null {{ data_type }} => if self.{field}.is_dirty() {{ record.set_null({:?})? }} else {{ record.hydrate({:?}, TypedValue::Null(data_type.clone()))? }},", column.name, column.name).unwrap();
303 writeln!(output, " FieldValue::Value {{ value }} => if self.{field}.is_dirty() {{ record.set({:?}, {typed_value})? }} else {{ record.hydrate({:?}, {typed_value})? }},", column.name, column.name).unwrap();
304 output.push_str(" }\n");
305 }
306 output.push_str(" Ok(record)\n }\n");
307 writeln!(output, " pub fn insert<S: OrmGeneratedRecordSession>(&mut self, session: S) -> Result<(), S::Error> {{ session.mutate_generated_record(self, RecordMutation::Insert) }}").unwrap();
308 writeln!(output, " pub fn save<S: OrmGeneratedRecordSession>(&mut self, session: S) -> Result<(), S::Error> {{ session.mutate_generated_record(self, RecordMutation::Save) }}").unwrap();
309 writeln!(output, " pub fn update<S: OrmGeneratedRecordSession>(&mut self, session: S) -> Result<(), S::Error> {{ session.mutate_generated_record(self, RecordMutation::Update) }}").unwrap();
310 writeln!(output, " pub fn delete<S: OrmGeneratedRecordSession>(&mut self, session: S) -> Result<(), S::Error> {{ session.mutate_generated_record(self, RecordMutation::Delete) }}").unwrap();
311 writeln!(output, " pub async fn insert_async<S: AsyncOrmGeneratedRecordSession>(&mut self, session: &mut S) -> Result<(), S::Error> {{ session.mutate_generated_record_async(self, RecordMutation::Insert).await }}").unwrap();
312 writeln!(output, " pub async fn save_async<S: AsyncOrmGeneratedRecordSession>(&mut self, session: &mut S) -> Result<(), S::Error> {{ session.mutate_generated_record_async(self, RecordMutation::Save).await }}").unwrap();
313 writeln!(output, " pub async fn update_async<S: AsyncOrmGeneratedRecordSession>(&mut self, session: &mut S) -> Result<(), S::Error> {{ session.mutate_generated_record_async(self, RecordMutation::Update).await }}").unwrap();
314 writeln!(output, " pub async fn delete_async<S: AsyncOrmGeneratedRecordSession>(&mut self, session: &mut S) -> Result<(), S::Error> {{ session.mutate_generated_record_async(self, RecordMutation::Delete).await }}").unwrap();
315 output.push_str("}\n\n");
316
317 writeln!(output, "impl GeneratedRecord for {record} {{").unwrap();
318 writeln!(output, " type Entity = {entity};").unwrap();
319 writeln!(output, " fn to_dynamic(&self, descriptor: &TableDescriptor) -> Result<DynamicRecord, GeneratedRecordError> {{ {record}::to_dynamic(self, descriptor) }}").unwrap();
320 output.push_str(" fn apply_dynamic(&mut self, record: &DynamicRecord) -> Result<(), GeneratedRecordError> {\n");
321 writeln!(output, " ensure_schema_fingerprint({entity}::SCHEMA_FINGERPRINT, &record.descriptor().fingerprint)?;").unwrap();
322 for column in &table.columns {
323 let field = rust_field_name(&column.name)?;
324 if let Some((target_table, target_column)) = column_reference(table, &column.name) {
325 let target_entity = rust_type_name(target_table)?;
326 writeln!(output, " self.{field}.hydrate(decode_generated_reference_field::<{target_entity}>({:?}, record.field({:?})?, &{}, {:?}, {:?})?);", column.name, column.name, rust_data_type(&column.data_type), target_table, target_column).unwrap();
327 } else {
328 writeln!(output, " self.{field}.hydrate(decode_generated_field({:?}, record.field({:?})?, &{})?);", column.name, column.name, rust_data_type(&column.data_type)).unwrap();
329 }
330 }
331 output.push_str(" Ok(())\n }\n}\n\n");
332 Ok(())
333}
334
335fn validate_reference_shapes(
336 table: &TableDescriptor,
337 tables: &[TableDescriptor],
338) -> Result<(), CodegenError> {
339 for constraint in &table.constraints {
340 let ConstraintDefinition::ForeignKey {
341 columns,
342 referenced_table,
343 referenced_columns,
344 ..
345 } = &constraint.definition
346 else {
347 continue;
348 };
349 if columns.len() != 1 || referenced_columns.len() != 1 {
350 return Err(CodegenError::UnsupportedReference {
351 table: table.name.clone(),
352 column: columns.join(","),
353 });
354 }
355 if !tables.iter().any(|table| table.name == *referenced_table) {
356 return Err(CodegenError::MissingReferenceTarget(
357 referenced_table.clone(),
358 ));
359 }
360 let source_column = table
361 .columns
362 .iter()
363 .find(|column| column.name == columns[0])
364 .ok_or_else(|| CodegenError::MissingReferenceSourceColumn {
365 table: table.name.clone(),
366 column: columns[0].clone(),
367 })?;
368 let target_table = tables
369 .iter()
370 .find(|table| table.name == *referenced_table)
371 .expect("target table presence checked");
372 let target_column = target_table
373 .columns
374 .iter()
375 .find(|column| column.name == referenced_columns[0])
376 .ok_or_else(|| CodegenError::MissingReferenceTargetColumn {
377 table: referenced_table.clone(),
378 column: referenced_columns[0].clone(),
379 })?;
380 if !reference_keys(target_table)
381 .iter()
382 .any(|(column, _)| *column == target_column.name)
383 {
384 return Err(CodegenError::UnsupportedReferenceTargetKey {
385 table: referenced_table.clone(),
386 column: referenced_columns[0].clone(),
387 });
388 }
389 if source_column.data_type != target_column.data_type {
390 return Err(CodegenError::ReferenceTypeMismatch {
391 source_table: table.name.clone(),
392 source_column: source_column.name.clone(),
393 source_type: source_column.data_type.clone(),
394 target_table: target_table.name.clone(),
395 target_column: target_column.name.clone(),
396 target_type: target_column.data_type.clone(),
397 });
398 }
399 }
400 Ok(())
401}
402
403fn column_reference<'a>(table: &'a TableDescriptor, column: &str) -> Option<(&'a str, &'a str)> {
404 table.constraints.iter().find_map(|constraint| {
405 let ConstraintDefinition::ForeignKey {
406 columns,
407 referenced_table,
408 referenced_columns,
409 ..
410 } = &constraint.definition
411 else {
412 return None;
413 };
414 (columns.as_slice() == [column] && referenced_columns.len() == 1)
415 .then(|| (referenced_table.as_str(), referenced_columns[0].as_str()))
416 })
417}
418
419fn primary_reference_key(table: &TableDescriptor) -> Option<&str> {
420 table.constraints.iter().find_map(|constraint| {
421 let ConstraintDefinition::PrimaryKey { columns } = &constraint.definition else {
422 return None;
423 };
424 (columns.len() == 1).then(|| columns[0].as_str())
425 })
426}
427
428fn reference_keys(table: &TableDescriptor) -> Vec<(&str, bool)> {
429 let mut keys = Vec::new();
430 for constraint in &table.constraints {
431 let (columns, is_primary) = match &constraint.definition {
432 ConstraintDefinition::PrimaryKey { columns } => (columns, true),
433 ConstraintDefinition::Unique { columns, .. } => (columns, false),
434 _ => continue,
435 };
436 if columns.len() == 1 {
437 if let Some(column) = table
438 .columns
439 .iter()
440 .find(|column| column.name == columns[0] && !column.nullable)
441 {
442 if !keys.iter().any(|(name, _)| *name == column.name) {
443 keys.push((column.name.as_str(), is_primary));
444 }
445 }
446 }
447 }
448 keys
449}
450
451fn rust_type(data_type: &DataTypeDescriptor) -> &'static str {
452 match data_type {
453 DataTypeDescriptor::Integer => "i64",
454 DataTypeDescriptor::Float | DataTypeDescriptor::DoublePrecision => "f64",
455 DataTypeDescriptor::Boolean => "bool",
456 DataTypeDescriptor::Json => "serde_json::Value",
457 DataTypeDescriptor::Bytes => "String",
458 DataTypeDescriptor::Vector { .. } => "Vec<f32>",
459 DataTypeDescriptor::Null
460 | DataTypeDescriptor::Text { .. }
461 | DataTypeDescriptor::Timestamp
462 | DataTypeDescriptor::CivilTimestamp
463 | DataTypeDescriptor::Time
464 | DataTypeDescriptor::Date
465 | DataTypeDescriptor::Uuid
466 | DataTypeDescriptor::Decimal { .. } => "String",
467 }
468}
469
470fn rust_data_type(data_type: &DataTypeDescriptor) -> String {
471 match data_type {
472 DataTypeDescriptor::Null => "DataTypeDescriptor::Null".to_string(),
473 DataTypeDescriptor::Integer => "DataTypeDescriptor::Integer".to_string(),
474 DataTypeDescriptor::Float => "DataTypeDescriptor::Float".to_string(),
475 DataTypeDescriptor::DoublePrecision => "DataTypeDescriptor::DoublePrecision".to_string(),
476 DataTypeDescriptor::Text { max_chars } => {
477 format!("DataTypeDescriptor::Text {{ max_chars: {max_chars:?} }}")
478 }
479 DataTypeDescriptor::Boolean => "DataTypeDescriptor::Boolean".to_string(),
480 DataTypeDescriptor::Timestamp => "DataTypeDescriptor::Timestamp".to_string(),
481 DataTypeDescriptor::CivilTimestamp => "DataTypeDescriptor::CivilTimestamp".to_string(),
482 DataTypeDescriptor::Time => "DataTypeDescriptor::Time".to_string(),
483 DataTypeDescriptor::Date => "DataTypeDescriptor::Date".to_string(),
484 DataTypeDescriptor::Json => "DataTypeDescriptor::Json".to_string(),
485 DataTypeDescriptor::Uuid => "DataTypeDescriptor::Uuid".to_string(),
486 DataTypeDescriptor::Bytes => "DataTypeDescriptor::Bytes".to_string(),
487 DataTypeDescriptor::Decimal { precision, scale } => {
488 format!("DataTypeDescriptor::Decimal {{ precision: {precision:?}, scale: {scale:?} }}")
489 }
490 DataTypeDescriptor::Vector { dimensions } => {
491 format!("DataTypeDescriptor::Vector {{ dimensions: {dimensions} }}")
492 }
493 }
494}
495
496fn typed_value_expression(value: &str, data_type: &DataTypeDescriptor) -> String {
497 match data_type {
498 DataTypeDescriptor::Integer => format!("TypedValue::Integer(*{value})"),
499 DataTypeDescriptor::Float | DataTypeDescriptor::DoublePrecision => {
500 format!("TypedValue::Float((*{value}).into())")
501 }
502 DataTypeDescriptor::Text { .. } => format!("TypedValue::Text({value}.clone())"),
503 DataTypeDescriptor::Boolean => format!("TypedValue::Boolean(*{value})"),
504 DataTypeDescriptor::Timestamp => format!("TypedValue::Timestamp({value}.clone())"),
505 DataTypeDescriptor::CivilTimestamp => {
506 format!("TypedValue::CivilTimestamp({value}.clone())")
507 }
508 DataTypeDescriptor::Time => format!("TypedValue::Time({value}.clone())"),
509 DataTypeDescriptor::Date => format!("TypedValue::Date({value}.clone())"),
510 DataTypeDescriptor::Json => format!("TypedValue::Json({value}.clone())"),
511 DataTypeDescriptor::Uuid => format!("TypedValue::Uuid({value}.clone())"),
512 DataTypeDescriptor::Bytes => format!("TypedValue::Bytes({value}.clone())"),
513 DataTypeDescriptor::Decimal { .. } => format!("TypedValue::Decimal({value}.clone())"),
514 DataTypeDescriptor::Vector { .. } => format!("TypedValue::Vector({value}.clone())"),
515 DataTypeDescriptor::Null => "TypedValue::Null(DataTypeDescriptor::Null)".to_string(),
516 }
517}
518
519fn owned_typed_value_expression(value: &str, data_type: &DataTypeDescriptor) -> String {
520 match data_type {
521 DataTypeDescriptor::Integer => format!("TypedValue::Integer({value})"),
522 DataTypeDescriptor::Float | DataTypeDescriptor::DoublePrecision => {
523 format!("TypedValue::Float({value}.into())")
524 }
525 DataTypeDescriptor::Text { .. } => format!("TypedValue::Text({value})"),
526 DataTypeDescriptor::Boolean => format!("TypedValue::Boolean({value})"),
527 DataTypeDescriptor::Timestamp => format!("TypedValue::Timestamp({value})"),
528 DataTypeDescriptor::CivilTimestamp => {
529 format!("TypedValue::CivilTimestamp({value})")
530 }
531 DataTypeDescriptor::Time => format!("TypedValue::Time({value})"),
532 DataTypeDescriptor::Date => format!("TypedValue::Date({value})"),
533 DataTypeDescriptor::Json => format!("TypedValue::Json({value})"),
534 DataTypeDescriptor::Uuid => format!("TypedValue::Uuid({value})"),
535 DataTypeDescriptor::Bytes => format!("TypedValue::Bytes({value})"),
536 DataTypeDescriptor::Decimal { .. } => format!("TypedValue::Decimal({value})"),
537 DataTypeDescriptor::Vector { .. } => format!("TypedValue::Vector({value})"),
538 DataTypeDescriptor::Null => "TypedValue::Null(DataTypeDescriptor::Null)".to_string(),
539 }
540}
541
542fn rust_type_name(identifier: &str) -> Result<String, CodegenError> {
543 let parts = identifier_parts(identifier)?;
544 Ok(parts.into_iter().map(capitalize).collect())
545}
546
547fn rust_const_name(identifier: &str) -> Result<String, CodegenError> {
548 Ok(identifier_parts(identifier)?.join("_").to_ascii_uppercase())
549}
550
551fn rust_field_name(identifier: &str) -> Result<String, CodegenError> {
552 let mut name = identifier_parts(identifier)?.join("_").to_ascii_lowercase();
553 if is_rust_keyword(&name) {
554 name.insert_str(0, "r#");
555 }
556 Ok(name)
557}
558
559fn identifier_parts(identifier: &str) -> Result<Vec<&str>, CodegenError> {
560 let parts: Vec<_> = identifier
561 .split(|character: char| !character.is_ascii_alphanumeric())
562 .filter(|part| !part.is_empty())
563 .collect();
564 if parts.is_empty() || parts[0].as_bytes().first().is_some_and(u8::is_ascii_digit) {
565 return Err(CodegenError::Identifier(identifier.to_string()));
566 }
567 Ok(parts)
568}
569
570fn capitalize(part: &str) -> String {
571 let mut chars = part.chars();
572 match chars.next() {
573 Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
574 None => String::new(),
575 }
576}
577
578fn is_rust_keyword(value: &str) -> bool {
579 matches!(
580 value,
581 "as" | "break"
582 | "const"
583 | "continue"
584 | "crate"
585 | "else"
586 | "enum"
587 | "extern"
588 | "false"
589 | "fn"
590 | "for"
591 | "if"
592 | "impl"
593 | "in"
594 | "let"
595 | "loop"
596 | "match"
597 | "mod"
598 | "move"
599 | "mut"
600 | "pub"
601 | "ref"
602 | "return"
603 | "self"
604 | "Self"
605 | "static"
606 | "struct"
607 | "super"
608 | "trait"
609 | "true"
610 | "type"
611 | "unsafe"
612 | "use"
613 | "where"
614 | "while"
615 | "async"
616 | "await"
617 | "dyn"
618 )
619}
620
621#[cfg(test)]
622mod tests {
623 use std::collections::BTreeMap;
624 use std::fs;
625 use std::process::Command;
626
627 use super::*;
628
629 #[test]
630 fn codegen_is_deterministic_offline_and_fingerprint_bound() {
631 let mut table = TableDescriptor {
632 catalog_id: "018f2b34-7a10-7cc2-8f3a-9d4b5c6d7e01".to_string(),
633 name: "people".to_string(),
634 schema_generation: 1,
635 fingerprint: String::new(),
636 created_at: "x".to_string(),
637 updated_at: "x".to_string(),
638 columns: vec![ColumnDescriptor {
639 ordinal: 0,
640 name: "id".to_string(),
641 data_type: DataTypeDescriptor::Integer,
642 nullable: false,
643 auto_increment: false,
644 default_expression: None,
645 extensions: BTreeMap::new(),
646 }],
647 constraints: vec![ConstraintDescriptor {
648 id: 1,
649 name: "pk_people".to_string(),
650 definition: ConstraintDefinition::PrimaryKey {
651 columns: vec!["id".to_string()],
652 },
653 }],
654 indexes: Vec::new(),
655 extensions: BTreeMap::new(),
656 };
657 table.refresh_fingerprint().unwrap();
658 let mut database = DatabaseDescriptor {
659 schema_generation: 1,
660 fingerprint: String::new(),
661 tables: vec![table],
662 views: Vec::new(),
663 procedures: Vec::new(),
664 extensions: BTreeMap::new(),
665 };
666 database.refresh_fingerprint().unwrap();
667 let envelope = DescriptorEnvelope::new(DescriptorKind::Database, database);
668 let first = generate_rust_database(&envelope).unwrap();
669 let second = generate_rust_database(&envelope).unwrap();
670 assert_eq!(first, second);
671 assert!(first
672 .source
673 .contains("pub const ID: TypedColumn<i64, Self>"));
674 assert!(first.source.contains("pub const ID: TypedKey<People, i64>"));
675 assert!(first.source.contains("pub fn reference(key: i64)"));
676
677 let mut stale = envelope.clone();
678 stale.payload.tables[0].columns[0].nullable = true;
679 assert!(matches!(
680 generate_rust_database(&stale),
681 Err(CodegenError::Fingerprint { .. })
682 ));
683 }
684
685 #[test]
686 fn codegen_rejects_unusable_or_type_mismatched_reference_targets() {
687 fn descriptor(
688 target_type: DataTypeDescriptor,
689 target_nullable: bool,
690 target_constraint: ConstraintDefinition,
691 target_column: &str,
692 ) -> DescriptorEnvelope<DatabaseDescriptor> {
693 let mut target = TableDescriptor {
694 catalog_id: "target-id".to_string(),
695 name: "targets".to_string(),
696 schema_generation: 1,
697 fingerprint: String::new(),
698 created_at: "x".to_string(),
699 updated_at: "x".to_string(),
700 columns: vec![ColumnDescriptor {
701 ordinal: 0,
702 name: "id".to_string(),
703 data_type: target_type,
704 nullable: target_nullable,
705 auto_increment: false,
706 default_expression: None,
707 extensions: BTreeMap::new(),
708 }],
709 constraints: vec![ConstraintDescriptor {
710 id: 1,
711 name: "target_key".to_string(),
712 definition: target_constraint,
713 }],
714 indexes: Vec::new(),
715 extensions: BTreeMap::new(),
716 };
717 target.refresh_fingerprint().unwrap();
718 let mut source = TableDescriptor {
719 catalog_id: "source-id".to_string(),
720 name: "sources".to_string(),
721 schema_generation: 1,
722 fingerprint: String::new(),
723 created_at: "x".to_string(),
724 updated_at: "x".to_string(),
725 columns: vec![ColumnDescriptor {
726 ordinal: 0,
727 name: "target_id".to_string(),
728 data_type: DataTypeDescriptor::Integer,
729 nullable: false,
730 auto_increment: false,
731 default_expression: None,
732 extensions: BTreeMap::new(),
733 }],
734 constraints: vec![ConstraintDescriptor {
735 id: 1,
736 name: "fk_sources_target_id___targets".to_string(),
737 definition: ConstraintDefinition::ForeignKey {
738 columns: vec!["target_id".to_string()],
739 referenced_table: "targets".to_string(),
740 referenced_columns: vec![target_column.to_string()],
741 on_delete: ForeignKeyActionDescriptor::Restrict,
742 on_update: ForeignKeyActionDescriptor::Restrict,
743 },
744 }],
745 indexes: Vec::new(),
746 extensions: BTreeMap::new(),
747 };
748 source.refresh_fingerprint().unwrap();
749 let mut database = DatabaseDescriptor {
750 schema_generation: 1,
751 fingerprint: String::new(),
752 tables: vec![target, source],
753 views: Vec::new(),
754 procedures: Vec::new(),
755 extensions: BTreeMap::new(),
756 };
757 database.refresh_fingerprint().unwrap();
758 DescriptorEnvelope::new(DescriptorKind::Database, database)
759 }
760
761 let missing = descriptor(
762 DataTypeDescriptor::Integer,
763 false,
764 ConstraintDefinition::PrimaryKey {
765 columns: vec!["id".to_string()],
766 },
767 "absent",
768 );
769 assert!(matches!(
770 generate_rust_database(&missing),
771 Err(CodegenError::MissingReferenceTargetColumn { .. })
772 ));
773
774 let ordinary = descriptor(
775 DataTypeDescriptor::Integer,
776 false,
777 ConstraintDefinition::Check {
778 column: Some("id".to_string()),
779 expression: "id > 0".to_string(),
780 ordinal: 1,
781 },
782 "id",
783 );
784 assert!(matches!(
785 generate_rust_database(&ordinary),
786 Err(CodegenError::UnsupportedReferenceTargetKey { .. })
787 ));
788
789 let nullable_unique = descriptor(
790 DataTypeDescriptor::Integer,
791 true,
792 ConstraintDefinition::Unique {
793 columns: vec!["id".to_string()],
794 owned_index: "uq_targets_id".to_string(),
795 },
796 "id",
797 );
798 assert!(matches!(
799 generate_rust_database(&nullable_unique),
800 Err(CodegenError::UnsupportedReferenceTargetKey { .. })
801 ));
802
803 let wrong_type = descriptor(
804 DataTypeDescriptor::Text { max_chars: None },
805 false,
806 ConstraintDefinition::PrimaryKey {
807 columns: vec!["id".to_string()],
808 },
809 "id",
810 );
811 assert!(matches!(
812 generate_rust_database(&wrong_type),
813 Err(CodegenError::ReferenceTypeMismatch { .. })
814 ));
815 }
816
817 #[test]
818 fn generated_source_compiles_as_an_independent_offline_crate() {
819 let mut table = TableDescriptor {
820 catalog_id: "018f2b34-7a10-7cc2-8f3a-9d4b5c6d7e01".to_string(),
821 name: "people".to_string(),
822 schema_generation: 1,
823 fingerprint: String::new(),
824 created_at: "x".to_string(),
825 updated_at: "x".to_string(),
826 columns: vec![
827 ColumnDescriptor {
828 ordinal: 0,
829 name: "id".to_string(),
830 data_type: DataTypeDescriptor::Integer,
831 nullable: false,
832 auto_increment: false,
833 default_expression: None,
834 extensions: BTreeMap::new(),
835 },
836 ColumnDescriptor {
837 ordinal: 1,
838 name: "metadata".to_string(),
839 data_type: DataTypeDescriptor::Json,
840 nullable: true,
841 auto_increment: false,
842 default_expression: None,
843 extensions: BTreeMap::new(),
844 },
845 ColumnDescriptor {
846 ordinal: 2,
847 name: "fio_id".to_string(),
848 data_type: DataTypeDescriptor::Integer,
849 nullable: true,
850 auto_increment: false,
851 default_expression: None,
852 extensions: BTreeMap::new(),
853 },
854 ColumnDescriptor {
855 ordinal: 3,
856 name: "measurement".to_string(),
857 data_type: DataTypeDescriptor::DoublePrecision,
858 nullable: false,
859 auto_increment: false,
860 default_expression: None,
861 extensions: BTreeMap::new(),
862 },
863 ],
864 constraints: vec![
865 ConstraintDescriptor {
866 id: 1,
867 name: "pk_people".to_string(),
868 definition: ConstraintDefinition::PrimaryKey {
869 columns: vec!["id".to_string()],
870 },
871 },
872 ConstraintDescriptor {
873 id: 2,
874 name: "fk_people_fio_id___fio".to_string(),
875 definition: ConstraintDefinition::ForeignKey {
876 columns: vec!["fio_id".to_string()],
877 referenced_table: "fio".to_string(),
878 referenced_columns: vec!["id".to_string()],
879 on_delete: ForeignKeyActionDescriptor::Restrict,
880 on_update: ForeignKeyActionDescriptor::Restrict,
881 },
882 },
883 ],
884 indexes: Vec::new(),
885 extensions: BTreeMap::new(),
886 };
887 table.refresh_fingerprint().unwrap();
888 let mut fio = TableDescriptor {
889 catalog_id: "018f2b34-7a10-7cc2-8f3a-9d4b5c6d7e02".to_string(),
890 name: "fio".to_string(),
891 schema_generation: 1,
892 fingerprint: String::new(),
893 created_at: "x".to_string(),
894 updated_at: "x".to_string(),
895 columns: vec![ColumnDescriptor {
896 ordinal: 0,
897 name: "id".to_string(),
898 data_type: DataTypeDescriptor::Integer,
899 nullable: false,
900 auto_increment: false,
901 default_expression: None,
902 extensions: BTreeMap::new(),
903 }],
904 constraints: vec![ConstraintDescriptor {
905 id: 1,
906 name: "pk_fio".to_string(),
907 definition: ConstraintDefinition::PrimaryKey {
908 columns: vec!["id".to_string()],
909 },
910 }],
911 indexes: Vec::new(),
912 extensions: BTreeMap::new(),
913 };
914 fio.refresh_fingerprint().unwrap();
915 let mut database = DatabaseDescriptor {
916 schema_generation: 1,
917 fingerprint: String::new(),
918 tables: vec![fio, table],
919 views: Vec::new(),
920 procedures: Vec::new(),
921 extensions: BTreeMap::new(),
922 };
923 database.refresh_fingerprint().unwrap();
924 let mut generated =
925 generate_rust_database(&DescriptorEnvelope::new(DescriptorKind::Database, database))
926 .unwrap();
927 generated.source.push_str(
928 "\n#[allow(dead_code)]\nfn compile_generated_insert<S: radixdb_orm::OrmGeneratedRecordSession>(mut record: PeopleRecord, session: S) -> Result<(), S::Error> { record.insert(session) }\n",
929 );
930 assert!(generated.source.contains("FieldState<Reference<Fio>>"));
931 generated.source.push_str(
932 "\n#[allow(dead_code)]\nfn compile_generated_get<S>(session: S) -> Result<Option<PeopleRecord>, S::Error> where S: radixdb_orm::OrmGeneratedQuerySession, S::Error: From<radixdb_orm::RecordError> + From<radixdb_orm::BuilderError> { People::get(1).optional(session) }\n",
933 );
934 generated.source.push_str(
935 "\n#[allow(dead_code)]\nfn compile_generated_null(mut record: PeopleRecord) { record.metadata.set_null(); record.metadata.unset(); }\n",
936 );
937 generated.source.push_str(
938 "\n#[allow(dead_code)]\nasync fn compile_generated_async<S>(mut record: PeopleRecord, session: &mut S) -> Result<Option<PeopleRecord>, <S as radixdb_orm::AsyncOrmGeneratedRecordSession>::Error> where S: radixdb_orm::AsyncOrmGeneratedRecordSession + radixdb_orm::AsyncOrmGeneratedQuerySession<Error = <S as radixdb_orm::AsyncOrmGeneratedRecordSession>::Error>, <S as radixdb_orm::AsyncOrmGeneratedRecordSession>::Error: From<radixdb_orm::RecordError> + From<radixdb_orm::BuilderError> { record.insert_async(session).await?; People::get(1).optional_async(session).await }\n",
939 );
940
941 let temp = std::env::temp_dir().join(format!("radixdb-orm-codegen-{}", std::process::id()));
942 let _ = fs::remove_dir_all(&temp);
943 fs::create_dir_all(temp.join("src")).unwrap();
944 let manifest_dir = env!("CARGO_MANIFEST_DIR").replace('\\', "\\\\");
945 fs::write(
946 temp.join("Cargo.toml"),
947 format!(
948 "[package]\nname = \"radixdb-orm-generated-probe\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\n[dependencies]\nradixdb-orm = {{ path = {:?} }}\nserde_json = \"1\"\n",
949 manifest_dir
950 ),
951 )
952 .unwrap();
953 fs::write(temp.join("src/lib.rs"), generated.source).unwrap();
954 let status = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
955 .args(["check", "--offline", "--quiet"])
956 .current_dir(&temp)
957 .env("CARGO_TARGET_DIR", temp.join("target"))
958 .status()
959 .unwrap();
960 let _ = fs::remove_dir_all(&temp);
961 assert!(status.success(), "generated Rust facade did not compile");
962 }
963}