//! Deterministic Rust facade generation from saved schema descriptors.
use std::fmt::Write;
use crate::*;
#[derive(Debug, thiserror::Error)]
pub enum CodegenError {
#[error(transparent)]
Descriptor(#[from] DescriptorError),
#[error(
"descriptor fingerprint mismatch for '{name}': expected {expected}, computed {actual}"
)]
Fingerprint {
name: String,
expected: String,
actual: String,
},
#[error("identifier '{0}' cannot be represented safely in generated Rust")]
Identifier(String),
#[error("reference '{table}.{column}' must use one source and one target column")]
UnsupportedReference { table: String, column: String },
#[error("reference target table '{0}' is absent from the descriptor")]
MissingReferenceTarget(String),
#[error("reference source column '{table}.{column}' is absent from the descriptor")]
MissingReferenceSourceColumn { table: String, column: String },
#[error("reference target column '{table}.{column}' is absent from the descriptor")]
MissingReferenceTargetColumn { table: String, column: String },
#[error(
"reference target '{table}.{column}' must be a one-column PRIMARY KEY or UNIQUE NOT NULL key"
)]
UnsupportedReferenceTargetKey { table: String, column: String },
#[error(
"reference type mismatch: '{source_table}.{source_column}' is {source_type:?}, but '{target_table}.{target_column}' is {target_type:?}"
)]
ReferenceTypeMismatch {
source_table: String,
source_column: String,
source_type: DataTypeDescriptor,
target_table: String,
target_column: String,
target_type: DataTypeDescriptor,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedRust {
pub source: String,
pub descriptor_fingerprint: String,
}
pub fn generate_rust_database(
descriptor: &DescriptorEnvelope<DatabaseDescriptor>,
) -> Result<GeneratedRust, CodegenError> {
if descriptor.descriptor != SCHEMA_DESCRIPTOR_VERSION
|| descriptor.kind != DescriptorKind::Database
{
return Err(CodegenError::Descriptor(DescriptorError::KindMismatch {
expected: DescriptorKind::Database,
actual: descriptor.kind,
}));
}
validate_database_fingerprints(&descriptor.payload)?;
let mut output = String::new();
output.push_str("// @generated by radixdb-orm; DO NOT EDIT.\n");
output.push_str("// Source: radixdb.schema.v1 descriptor. No live database was accessed.\n\n");
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");
writeln!(
output,
"pub const DATABASE_SCHEMA_FINGERPRINT: &str = {:?};\n",
descriptor.payload.fingerprint
)
.unwrap();
for table in &descriptor.payload.tables {
render_table(&mut output, table, &descriptor.payload.tables)?;
}
Ok(GeneratedRust {
source: output,
descriptor_fingerprint: descriptor.payload.fingerprint.clone(),
})
}
fn validate_database_fingerprints(database: &DatabaseDescriptor) -> Result<(), CodegenError> {
for table in &database.tables {
let actual = table.computed_fingerprint()?;
if table.fingerprint != actual {
return Err(CodegenError::Fingerprint {
name: table.name.clone(),
expected: table.fingerprint.clone(),
actual,
});
}
}
let actual = database.computed_fingerprint()?;
if database.fingerprint != actual {
return Err(CodegenError::Fingerprint {
name: "database".to_string(),
expected: database.fingerprint.clone(),
actual,
});
}
Ok(())
}
fn render_table(
output: &mut String,
table: &TableDescriptor,
tables: &[TableDescriptor],
) -> Result<(), CodegenError> {
validate_reference_shapes(table, tables)?;
let entity = rust_type_name(&table.name)?;
let record = format!("{entity}Record");
writeln!(
output,
"#[derive(Debug, Clone, PartialEq)]\npub struct {record} {{"
)
.unwrap();
for column in &table.columns {
let field_type = match column_reference(table, &column.name) {
Some((target_table, _)) => rust_type_name(target_table)?,
None => rust_type(&column.data_type).to_string(),
};
writeln!(
output,
" pub {}: FieldState<{}>,",
rust_field_name(&column.name)?,
match column_reference(table, &column.name) {
Some(_) => format!("Reference<{field_type}>"),
None => field_type,
}
)
.unwrap();
}
output.push_str("}\n\n");
writeln!(output, "impl Default for {record} {{").unwrap();
output.push_str(" fn default() -> Self {\n Self {\n");
for column in &table.columns {
writeln!(
output,
" {}: FieldState::typed({}),",
rust_field_name(&column.name)?,
rust_data_type(&column.data_type)
)
.unwrap();
}
output.push_str(" }\n }\n}\n\n");
writeln!(
output,
"#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub struct {entity};"
)
.unwrap();
writeln!(output, "impl GeneratedEntity for {entity} {{").unwrap();
writeln!(output, " type Record = {record};").unwrap();
writeln!(output, " const TABLE: &'static str = {:?};", table.name).unwrap();
writeln!(
output,
" const CATALOG_ID: &'static str = {:?};",
table.catalog_id
)
.unwrap();
writeln!(
output,
" const SCHEMA_FINGERPRINT: &'static str = {:?};",
table.fingerprint
)
.unwrap();
output.push_str("}\n\n");
writeln!(output, "impl {entity} {{").unwrap();
for column in &table.columns {
let column_type = match column_reference(table, &column.name) {
Some((target_table, _)) => format!("Reference<{}>", rust_type_name(target_table)?),
None => rust_type(&column.data_type).to_string(),
};
writeln!(
output,
" pub const {}: TypedColumn<{}, Self> = TypedColumn::new({:?}, {:?}, {}, {});",
rust_const_name(&column.name)?,
column_type,
table.name,
column.name,
rust_data_type(&column.data_type),
column.nullable
)
.unwrap();
}
writeln!(
output,
" pub fn new() -> {record} {{ {record}::default() }}"
)
.unwrap();
writeln!(output, " pub fn table() -> Relation {{ Relation::Table {{ name: {:?}.to_string(), alias: None }} }}", table.name).unwrap();
output.push_str(
" pub fn query() -> QueryBuilder { QueryBuilder::from_relation(Self::table()) }\n",
);
writeln!(
output,
" pub fn insert() -> InsertBuilder {{ InsertBuilder::new({:?}) }}",
table.name
)
.unwrap();
writeln!(
output,
" pub fn upsert() -> InsertBuilder {{ InsertBuilder::new({:?}) }}",
table.name
)
.unwrap();
writeln!(
output,
" pub fn update() -> UpdateBuilder {{ UpdateBuilder::new({:?}) }}",
table.name
)
.unwrap();
writeln!(
output,
" pub fn delete() -> DeleteBuilder {{ DeleteBuilder::new({:?}) }}",
table.name
)
.unwrap();
if let Some(primary_key) = primary_reference_key(table) {
let column = table
.columns
.iter()
.find(|candidate| candidate.name == primary_key)
.expect("constraint column exists");
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();
}
for (key, is_primary) in reference_keys(table) {
let column = table
.columns
.iter()
.find(|candidate| candidate.name == key)
.expect("constraint column exists");
let method = if is_primary {
"reference".to_string()
} else {
format!("reference_by_{}", rust_field_name(key)?)
};
let encoder = format!("encode_{}_key", rust_field_name(key)?);
writeln!(
output,
" fn {encoder}(key: {}) -> TypedValue {{ {} }}",
rust_type(&column.data_type),
owned_typed_value_expression("key", &column.data_type)
)
.unwrap();
writeln!(
output,
" pub fn {method}(key: {}) -> Reference<Self> {{ {}Keys::{}.reference(key) }}",
rust_type(&column.data_type),
entity,
rust_const_name(key)?
)
.unwrap();
}
output.push_str("}\n\n");
if !reference_keys(table).is_empty() {
writeln!(
output,
"#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub struct {entity}Keys;"
)
.unwrap();
writeln!(output, "impl {entity}Keys {{").unwrap();
for (key, is_primary) in reference_keys(table) {
let column = table
.columns
.iter()
.find(|candidate| candidate.name == key)
.expect("constraint column exists");
writeln!(
output,
" pub const {}: TypedKey<{entity}, {}> = TypedKey::new({entity}::{}, {}, {entity}::encode_{}_key);",
rust_const_name(key)?,
rust_type(&column.data_type),
rust_const_name(key)?,
is_primary,
rust_field_name(key)?,
)
.unwrap();
}
output.push_str("}\n\n");
}
writeln!(output, "impl {record} {{").unwrap();
writeln!(output, " pub fn to_dynamic(&self, descriptor: &TableDescriptor) -> Result<DynamicRecord, GeneratedRecordError> {{").unwrap();
writeln!(output, " ensure_schema_fingerprint({entity}::SCHEMA_FINGERPRINT, &descriptor.fingerprint)?;").unwrap();
output.push_str(" let mut record = DynamicRecord::new(descriptor.clone());\n");
for column in &table.columns {
let field = rust_field_name(&column.name)?;
let typed_value = match column_reference(table, &column.name) {
Some(_) => "value.key().clone()".to_string(),
None => typed_value_expression("value", &column.data_type),
};
writeln!(output, " match self.{field}.value() {{").unwrap();
output.push_str(" FieldValue::Omitted => {}\n");
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();
writeln!(output, " FieldValue::Value {{ value }} => if self.{field}.is_dirty() {{ record.set({:?}, {typed_value})? }} else {{ record.hydrate({:?}, {typed_value})? }},", column.name, column.name).unwrap();
output.push_str(" }\n");
}
output.push_str(" Ok(record)\n }\n");
writeln!(output, " pub fn insert<S: OrmGeneratedRecordSession>(&mut self, session: S) -> Result<(), S::Error> {{ session.mutate_generated_record(self, RecordMutation::Insert) }}").unwrap();
writeln!(output, " pub fn save<S: OrmGeneratedRecordSession>(&mut self, session: S) -> Result<(), S::Error> {{ session.mutate_generated_record(self, RecordMutation::Save) }}").unwrap();
writeln!(output, " pub fn update<S: OrmGeneratedRecordSession>(&mut self, session: S) -> Result<(), S::Error> {{ session.mutate_generated_record(self, RecordMutation::Update) }}").unwrap();
writeln!(output, " pub fn delete<S: OrmGeneratedRecordSession>(&mut self, session: S) -> Result<(), S::Error> {{ session.mutate_generated_record(self, RecordMutation::Delete) }}").unwrap();
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();
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();
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();
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();
output.push_str("}\n\n");
writeln!(output, "impl GeneratedRecord for {record} {{").unwrap();
writeln!(output, " type Entity = {entity};").unwrap();
writeln!(output, " fn to_dynamic(&self, descriptor: &TableDescriptor) -> Result<DynamicRecord, GeneratedRecordError> {{ {record}::to_dynamic(self, descriptor) }}").unwrap();
output.push_str(" fn apply_dynamic(&mut self, record: &DynamicRecord) -> Result<(), GeneratedRecordError> {\n");
writeln!(output, " ensure_schema_fingerprint({entity}::SCHEMA_FINGERPRINT, &record.descriptor().fingerprint)?;").unwrap();
for column in &table.columns {
let field = rust_field_name(&column.name)?;
if let Some((target_table, target_column)) = column_reference(table, &column.name) {
let target_entity = rust_type_name(target_table)?;
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();
} else {
writeln!(output, " self.{field}.hydrate(decode_generated_field({:?}, record.field({:?})?, &{})?);", column.name, column.name, rust_data_type(&column.data_type)).unwrap();
}
}
output.push_str(" Ok(())\n }\n}\n\n");
Ok(())
}
fn validate_reference_shapes(
table: &TableDescriptor,
tables: &[TableDescriptor],
) -> Result<(), CodegenError> {
for constraint in &table.constraints {
let ConstraintDefinition::ForeignKey {
columns,
referenced_table,
referenced_columns,
..
} = &constraint.definition
else {
continue;
};
if columns.len() != 1 || referenced_columns.len() != 1 {
return Err(CodegenError::UnsupportedReference {
table: table.name.clone(),
column: columns.join(","),
});
}
if !tables.iter().any(|table| table.name == *referenced_table) {
return Err(CodegenError::MissingReferenceTarget(
referenced_table.clone(),
));
}
let source_column = table
.columns
.iter()
.find(|column| column.name == columns[0])
.ok_or_else(|| CodegenError::MissingReferenceSourceColumn {
table: table.name.clone(),
column: columns[0].clone(),
})?;
let target_table = tables
.iter()
.find(|table| table.name == *referenced_table)
.expect("target table presence checked");
let target_column = target_table
.columns
.iter()
.find(|column| column.name == referenced_columns[0])
.ok_or_else(|| CodegenError::MissingReferenceTargetColumn {
table: referenced_table.clone(),
column: referenced_columns[0].clone(),
})?;
if !reference_keys(target_table)
.iter()
.any(|(column, _)| *column == target_column.name)
{
return Err(CodegenError::UnsupportedReferenceTargetKey {
table: referenced_table.clone(),
column: referenced_columns[0].clone(),
});
}
if source_column.data_type != target_column.data_type {
return Err(CodegenError::ReferenceTypeMismatch {
source_table: table.name.clone(),
source_column: source_column.name.clone(),
source_type: source_column.data_type.clone(),
target_table: target_table.name.clone(),
target_column: target_column.name.clone(),
target_type: target_column.data_type.clone(),
});
}
}
Ok(())
}
fn column_reference<'a>(table: &'a TableDescriptor, column: &str) -> Option<(&'a str, &'a str)> {
table.constraints.iter().find_map(|constraint| {
let ConstraintDefinition::ForeignKey {
columns,
referenced_table,
referenced_columns,
..
} = &constraint.definition
else {
return None;
};
(columns.as_slice() == [column] && referenced_columns.len() == 1)
.then(|| (referenced_table.as_str(), referenced_columns[0].as_str()))
})
}
fn primary_reference_key(table: &TableDescriptor) -> Option<&str> {
table.constraints.iter().find_map(|constraint| {
let ConstraintDefinition::PrimaryKey { columns } = &constraint.definition else {
return None;
};
(columns.len() == 1).then(|| columns[0].as_str())
})
}
fn reference_keys(table: &TableDescriptor) -> Vec<(&str, bool)> {
let mut keys = Vec::new();
for constraint in &table.constraints {
let (columns, is_primary) = match &constraint.definition {
ConstraintDefinition::PrimaryKey { columns } => (columns, true),
ConstraintDefinition::Unique { columns, .. } => (columns, false),
_ => continue,
};
if columns.len() == 1 {
if let Some(column) = table
.columns
.iter()
.find(|column| column.name == columns[0] && !column.nullable)
{
if !keys.iter().any(|(name, _)| *name == column.name) {
keys.push((column.name.as_str(), is_primary));
}
}
}
}
keys
}
fn rust_type(data_type: &DataTypeDescriptor) -> &'static str {
match data_type {
DataTypeDescriptor::Integer => "i64",
DataTypeDescriptor::Float => "f64",
DataTypeDescriptor::Boolean => "bool",
DataTypeDescriptor::Json => "serde_json::Value",
DataTypeDescriptor::Bytes => "String",
DataTypeDescriptor::Vector { .. } => "Vec<f32>",
DataTypeDescriptor::Null
| DataTypeDescriptor::Text
| DataTypeDescriptor::Timestamp
| DataTypeDescriptor::Date
| DataTypeDescriptor::Uuid
| DataTypeDescriptor::Decimal { .. } => "String",
}
}
fn rust_data_type(data_type: &DataTypeDescriptor) -> String {
match data_type {
DataTypeDescriptor::Null => "DataTypeDescriptor::Null".to_string(),
DataTypeDescriptor::Integer => "DataTypeDescriptor::Integer".to_string(),
DataTypeDescriptor::Float => "DataTypeDescriptor::Float".to_string(),
DataTypeDescriptor::Text => "DataTypeDescriptor::Text".to_string(),
DataTypeDescriptor::Boolean => "DataTypeDescriptor::Boolean".to_string(),
DataTypeDescriptor::Timestamp => "DataTypeDescriptor::Timestamp".to_string(),
DataTypeDescriptor::Date => "DataTypeDescriptor::Date".to_string(),
DataTypeDescriptor::Json => "DataTypeDescriptor::Json".to_string(),
DataTypeDescriptor::Uuid => "DataTypeDescriptor::Uuid".to_string(),
DataTypeDescriptor::Bytes => "DataTypeDescriptor::Bytes".to_string(),
DataTypeDescriptor::Decimal { precision, scale } => {
format!("DataTypeDescriptor::Decimal {{ precision: {precision:?}, scale: {scale:?} }}")
}
DataTypeDescriptor::Vector { dimensions } => {
format!("DataTypeDescriptor::Vector {{ dimensions: {dimensions} }}")
}
}
}
fn typed_value_expression(value: &str, data_type: &DataTypeDescriptor) -> String {
match data_type {
DataTypeDescriptor::Integer => format!("TypedValue::Integer(*{value})"),
DataTypeDescriptor::Float => format!("TypedValue::Float((*{value}).into())"),
DataTypeDescriptor::Text => format!("TypedValue::Text({value}.clone())"),
DataTypeDescriptor::Boolean => format!("TypedValue::Boolean(*{value})"),
DataTypeDescriptor::Timestamp => format!("TypedValue::Timestamp({value}.clone())"),
DataTypeDescriptor::Date => format!("TypedValue::Date({value}.clone())"),
DataTypeDescriptor::Json => format!("TypedValue::Json({value}.clone())"),
DataTypeDescriptor::Uuid => format!("TypedValue::Uuid({value}.clone())"),
DataTypeDescriptor::Bytes => format!("TypedValue::Bytes({value}.clone())"),
DataTypeDescriptor::Decimal { .. } => format!("TypedValue::Decimal({value}.clone())"),
DataTypeDescriptor::Vector { .. } => format!("TypedValue::Vector({value}.clone())"),
DataTypeDescriptor::Null => "TypedValue::Null(DataTypeDescriptor::Null)".to_string(),
}
}
fn owned_typed_value_expression(value: &str, data_type: &DataTypeDescriptor) -> String {
match data_type {
DataTypeDescriptor::Integer => format!("TypedValue::Integer({value})"),
DataTypeDescriptor::Float => format!("TypedValue::Float({value}.into())"),
DataTypeDescriptor::Text => format!("TypedValue::Text({value})"),
DataTypeDescriptor::Boolean => format!("TypedValue::Boolean({value})"),
DataTypeDescriptor::Timestamp => format!("TypedValue::Timestamp({value})"),
DataTypeDescriptor::Date => format!("TypedValue::Date({value})"),
DataTypeDescriptor::Json => format!("TypedValue::Json({value})"),
DataTypeDescriptor::Uuid => format!("TypedValue::Uuid({value})"),
DataTypeDescriptor::Bytes => format!("TypedValue::Bytes({value})"),
DataTypeDescriptor::Decimal { .. } => format!("TypedValue::Decimal({value})"),
DataTypeDescriptor::Vector { .. } => format!("TypedValue::Vector({value})"),
DataTypeDescriptor::Null => "TypedValue::Null(DataTypeDescriptor::Null)".to_string(),
}
}
fn rust_type_name(identifier: &str) -> Result<String, CodegenError> {
let parts = identifier_parts(identifier)?;
Ok(parts.into_iter().map(capitalize).collect())
}
fn rust_const_name(identifier: &str) -> Result<String, CodegenError> {
Ok(identifier_parts(identifier)?.join("_").to_ascii_uppercase())
}
fn rust_field_name(identifier: &str) -> Result<String, CodegenError> {
let mut name = identifier_parts(identifier)?.join("_").to_ascii_lowercase();
if is_rust_keyword(&name) {
name.insert_str(0, "r#");
}
Ok(name)
}
fn identifier_parts(identifier: &str) -> Result<Vec<&str>, CodegenError> {
let parts: Vec<_> = identifier
.split(|character: char| !character.is_ascii_alphanumeric())
.filter(|part| !part.is_empty())
.collect();
if parts.is_empty() || parts[0].as_bytes().first().is_some_and(u8::is_ascii_digit) {
return Err(CodegenError::Identifier(identifier.to_string()));
}
Ok(parts)
}
fn capitalize(part: &str) -> String {
let mut chars = part.chars();
match chars.next() {
Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
None => String::new(),
}
}
fn is_rust_keyword(value: &str) -> bool {
matches!(
value,
"as" | "break"
| "const"
| "continue"
| "crate"
| "else"
| "enum"
| "extern"
| "false"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "match"
| "mod"
| "move"
| "mut"
| "pub"
| "ref"
| "return"
| "self"
| "Self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "unsafe"
| "use"
| "where"
| "while"
| "async"
| "await"
| "dyn"
)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::fs;
use std::process::Command;
use super::*;
#[test]
fn codegen_is_deterministic_offline_and_fingerprint_bound() {
let mut table = TableDescriptor {
catalog_id: "018f2b34-7a10-7cc2-8f3a-9d4b5c6d7e01".to_string(),
name: "people".to_string(),
schema_generation: 1,
fingerprint: String::new(),
created_at: "x".to_string(),
updated_at: "x".to_string(),
columns: vec![ColumnDescriptor {
ordinal: 0,
name: "id".to_string(),
data_type: DataTypeDescriptor::Integer,
nullable: false,
auto_increment: false,
default_expression: None,
extensions: BTreeMap::new(),
}],
constraints: vec![ConstraintDescriptor {
id: 1,
name: "pk_people".to_string(),
definition: ConstraintDefinition::PrimaryKey {
columns: vec!["id".to_string()],
},
}],
indexes: Vec::new(),
extensions: BTreeMap::new(),
};
table.refresh_fingerprint().unwrap();
let mut database = DatabaseDescriptor {
schema_generation: 1,
fingerprint: String::new(),
tables: vec![table],
views: Vec::new(),
extensions: BTreeMap::new(),
};
database.refresh_fingerprint().unwrap();
let envelope = DescriptorEnvelope::new(DescriptorKind::Database, database);
let first = generate_rust_database(&envelope).unwrap();
let second = generate_rust_database(&envelope).unwrap();
assert_eq!(first, second);
assert!(first
.source
.contains("pub const ID: TypedColumn<i64, Self>"));
assert!(first.source.contains("pub const ID: TypedKey<People, i64>"));
assert!(first.source.contains("pub fn reference(key: i64)"));
let mut stale = envelope.clone();
stale.payload.tables[0].columns[0].nullable = true;
assert!(matches!(
generate_rust_database(&stale),
Err(CodegenError::Fingerprint { .. })
));
}
#[test]
fn codegen_rejects_unusable_or_type_mismatched_reference_targets() {
fn descriptor(
target_type: DataTypeDescriptor,
target_nullable: bool,
target_constraint: ConstraintDefinition,
target_column: &str,
) -> DescriptorEnvelope<DatabaseDescriptor> {
let mut target = TableDescriptor {
catalog_id: "target-id".to_string(),
name: "targets".to_string(),
schema_generation: 1,
fingerprint: String::new(),
created_at: "x".to_string(),
updated_at: "x".to_string(),
columns: vec![ColumnDescriptor {
ordinal: 0,
name: "id".to_string(),
data_type: target_type,
nullable: target_nullable,
auto_increment: false,
default_expression: None,
extensions: BTreeMap::new(),
}],
constraints: vec![ConstraintDescriptor {
id: 1,
name: "target_key".to_string(),
definition: target_constraint,
}],
indexes: Vec::new(),
extensions: BTreeMap::new(),
};
target.refresh_fingerprint().unwrap();
let mut source = TableDescriptor {
catalog_id: "source-id".to_string(),
name: "sources".to_string(),
schema_generation: 1,
fingerprint: String::new(),
created_at: "x".to_string(),
updated_at: "x".to_string(),
columns: vec![ColumnDescriptor {
ordinal: 0,
name: "target_id".to_string(),
data_type: DataTypeDescriptor::Integer,
nullable: false,
auto_increment: false,
default_expression: None,
extensions: BTreeMap::new(),
}],
constraints: vec![ConstraintDescriptor {
id: 1,
name: "fk_sources_target_id___targets".to_string(),
definition: ConstraintDefinition::ForeignKey {
columns: vec!["target_id".to_string()],
referenced_table: "targets".to_string(),
referenced_columns: vec![target_column.to_string()],
on_delete: ForeignKeyActionDescriptor::Restrict,
on_update: ForeignKeyActionDescriptor::Restrict,
},
}],
indexes: Vec::new(),
extensions: BTreeMap::new(),
};
source.refresh_fingerprint().unwrap();
let mut database = DatabaseDescriptor {
schema_generation: 1,
fingerprint: String::new(),
tables: vec![target, source],
views: Vec::new(),
extensions: BTreeMap::new(),
};
database.refresh_fingerprint().unwrap();
DescriptorEnvelope::new(DescriptorKind::Database, database)
}
let missing = descriptor(
DataTypeDescriptor::Integer,
false,
ConstraintDefinition::PrimaryKey {
columns: vec!["id".to_string()],
},
"absent",
);
assert!(matches!(
generate_rust_database(&missing),
Err(CodegenError::MissingReferenceTargetColumn { .. })
));
let ordinary = descriptor(
DataTypeDescriptor::Integer,
false,
ConstraintDefinition::Check {
column: Some("id".to_string()),
expression: "id > 0".to_string(),
ordinal: 1,
},
"id",
);
assert!(matches!(
generate_rust_database(&ordinary),
Err(CodegenError::UnsupportedReferenceTargetKey { .. })
));
let nullable_unique = descriptor(
DataTypeDescriptor::Integer,
true,
ConstraintDefinition::Unique {
columns: vec!["id".to_string()],
owned_index: "uq_targets_id".to_string(),
},
"id",
);
assert!(matches!(
generate_rust_database(&nullable_unique),
Err(CodegenError::UnsupportedReferenceTargetKey { .. })
));
let wrong_type = descriptor(
DataTypeDescriptor::Text,
false,
ConstraintDefinition::PrimaryKey {
columns: vec!["id".to_string()],
},
"id",
);
assert!(matches!(
generate_rust_database(&wrong_type),
Err(CodegenError::ReferenceTypeMismatch { .. })
));
}
#[test]
fn generated_source_compiles_as_an_independent_offline_crate() {
let mut table = TableDescriptor {
catalog_id: "018f2b34-7a10-7cc2-8f3a-9d4b5c6d7e01".to_string(),
name: "people".to_string(),
schema_generation: 1,
fingerprint: String::new(),
created_at: "x".to_string(),
updated_at: "x".to_string(),
columns: vec![
ColumnDescriptor {
ordinal: 0,
name: "id".to_string(),
data_type: DataTypeDescriptor::Integer,
nullable: false,
auto_increment: false,
default_expression: None,
extensions: BTreeMap::new(),
},
ColumnDescriptor {
ordinal: 1,
name: "metadata".to_string(),
data_type: DataTypeDescriptor::Json,
nullable: true,
auto_increment: false,
default_expression: None,
extensions: BTreeMap::new(),
},
ColumnDescriptor {
ordinal: 2,
name: "fio_id".to_string(),
data_type: DataTypeDescriptor::Integer,
nullable: true,
auto_increment: false,
default_expression: None,
extensions: BTreeMap::new(),
},
],
constraints: vec![
ConstraintDescriptor {
id: 1,
name: "pk_people".to_string(),
definition: ConstraintDefinition::PrimaryKey {
columns: vec!["id".to_string()],
},
},
ConstraintDescriptor {
id: 2,
name: "fk_people_fio_id___fio".to_string(),
definition: ConstraintDefinition::ForeignKey {
columns: vec!["fio_id".to_string()],
referenced_table: "fio".to_string(),
referenced_columns: vec!["id".to_string()],
on_delete: ForeignKeyActionDescriptor::Restrict,
on_update: ForeignKeyActionDescriptor::Restrict,
},
},
],
indexes: Vec::new(),
extensions: BTreeMap::new(),
};
table.refresh_fingerprint().unwrap();
let mut fio = TableDescriptor {
catalog_id: "018f2b34-7a10-7cc2-8f3a-9d4b5c6d7e02".to_string(),
name: "fio".to_string(),
schema_generation: 1,
fingerprint: String::new(),
created_at: "x".to_string(),
updated_at: "x".to_string(),
columns: vec![ColumnDescriptor {
ordinal: 0,
name: "id".to_string(),
data_type: DataTypeDescriptor::Integer,
nullable: false,
auto_increment: false,
default_expression: None,
extensions: BTreeMap::new(),
}],
constraints: vec![ConstraintDescriptor {
id: 1,
name: "pk_fio".to_string(),
definition: ConstraintDefinition::PrimaryKey {
columns: vec!["id".to_string()],
},
}],
indexes: Vec::new(),
extensions: BTreeMap::new(),
};
fio.refresh_fingerprint().unwrap();
let mut database = DatabaseDescriptor {
schema_generation: 1,
fingerprint: String::new(),
tables: vec![fio, table],
views: Vec::new(),
extensions: BTreeMap::new(),
};
database.refresh_fingerprint().unwrap();
let mut generated =
generate_rust_database(&DescriptorEnvelope::new(DescriptorKind::Database, database))
.unwrap();
generated.source.push_str(
"\n#[allow(dead_code)]\nfn compile_generated_insert<S: radixdb_orm::OrmGeneratedRecordSession>(mut record: PeopleRecord, session: S) -> Result<(), S::Error> { record.insert(session) }\n",
);
assert!(generated.source.contains("FieldState<Reference<Fio>>"));
generated.source.push_str(
"\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",
);
generated.source.push_str(
"\n#[allow(dead_code)]\nfn compile_generated_null(mut record: PeopleRecord) { record.metadata.set_null(); record.metadata.unset(); }\n",
);
generated.source.push_str(
"\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",
);
let temp = std::env::temp_dir().join(format!("radixdb-orm-codegen-{}", std::process::id()));
let _ = fs::remove_dir_all(&temp);
fs::create_dir_all(temp.join("src")).unwrap();
let manifest_dir = env!("CARGO_MANIFEST_DIR").replace('\\', "\\\\");
fs::write(
temp.join("Cargo.toml"),
format!(
"[package]\nname = \"radixdb-orm-generated-probe\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\n[dependencies]\nradixdb-orm = {{ path = {:?} }}\nserde_json = \"1\"\n",
manifest_dir
),
)
.unwrap();
fs::write(temp.join("src/lib.rs"), generated.source).unwrap();
let status = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
.args(["check", "--offline", "--quiet"])
.current_dir(&temp)
.env("CARGO_TARGET_DIR", temp.join("target"))
.status()
.unwrap();
let _ = fs::remove_dir_all(&temp);
assert!(status.success(), "generated Rust facade did not compile");
}
}