Skip to main content

forge_core/schema/
registry.rs

1//! Schema registration system.
2//!
3//! `BTreeMap` storage ensures deterministic iteration order for codegen and migration diffs.
4//! `RwLock` supports the registration phase; the registry is effectively read-only after startup.
5
6use std::collections::BTreeMap;
7use std::sync::RwLock;
8
9use super::function::FunctionDef;
10use super::model::TableDef;
11
12const LOCK_POISONED: &str = "schema registry lock poisoned";
13
14/// Global registry of all schema definitions, populated by proc macros.
15pub struct SchemaRegistry {
16    tables: RwLock<BTreeMap<String, TableDef>>,
17    enums: RwLock<BTreeMap<String, EnumDef>>,
18    functions: RwLock<BTreeMap<String, FunctionDef>>,
19}
20
21impl SchemaRegistry {
22    /// Create a new empty registry.
23    pub fn new() -> Self {
24        Self {
25            tables: RwLock::new(BTreeMap::new()),
26            enums: RwLock::new(BTreeMap::new()),
27            functions: RwLock::new(BTreeMap::new()),
28        }
29    }
30
31    /// Register a table definition.
32    pub fn register_table(&self, table: TableDef) {
33        let mut tables = self.tables.write().expect(LOCK_POISONED);
34        tables.insert(table.name.clone(), table);
35    }
36
37    /// Register an enum definition.
38    pub fn register_enum(&self, enum_def: EnumDef) {
39        let mut enums = self.enums.write().expect(LOCK_POISONED);
40        enums.insert(enum_def.name.clone(), enum_def);
41    }
42
43    /// Register a function definition.
44    pub fn register_function(&self, func: FunctionDef) {
45        let mut functions = self.functions.write().expect(LOCK_POISONED);
46        functions.insert(func.name.clone(), func);
47    }
48
49    /// Get a table by name.
50    pub fn get_table(&self, name: &str) -> Option<TableDef> {
51        let tables = self.tables.read().expect(LOCK_POISONED);
52        tables.get(name).cloned()
53    }
54
55    /// Get an enum by name.
56    pub fn get_enum(&self, name: &str) -> Option<EnumDef> {
57        let enums = self.enums.read().expect(LOCK_POISONED);
58        enums.get(name).cloned()
59    }
60
61    /// Get a function by name.
62    pub fn get_function(&self, name: &str) -> Option<FunctionDef> {
63        let functions = self.functions.read().expect(LOCK_POISONED);
64        functions.get(name).cloned()
65    }
66
67    /// Get all registered tables.
68    pub fn all_tables(&self) -> Vec<TableDef> {
69        let tables = self.tables.read().expect(LOCK_POISONED);
70        tables.values().cloned().collect()
71    }
72
73    /// Get all registered enums.
74    pub fn all_enums(&self) -> Vec<EnumDef> {
75        let enums = self.enums.read().expect(LOCK_POISONED);
76        enums.values().cloned().collect()
77    }
78
79    /// Get all registered functions.
80    pub fn all_functions(&self) -> Vec<FunctionDef> {
81        let functions = self.functions.read().expect(LOCK_POISONED);
82        functions.values().cloned().collect()
83    }
84
85    /// Clear all registrations (useful for testing).
86    pub fn clear(&self) {
87        self.tables.write().expect(LOCK_POISONED).clear();
88        self.enums.write().expect(LOCK_POISONED).clear();
89        self.functions.write().expect(LOCK_POISONED).clear();
90    }
91}
92
93impl Default for SchemaRegistry {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99/// Enum type definition.
100#[derive(Debug, Clone)]
101pub struct EnumDef {
102    /// Enum name in Rust.
103    pub name: String,
104
105    /// Type name in SQL (lowercase).
106    pub sql_name: String,
107
108    /// Enum variants.
109    pub variants: Vec<EnumVariant>,
110
111    /// Documentation comment.
112    pub doc: Option<String>,
113}
114
115impl EnumDef {
116    /// Create a new enum definition.
117    pub fn new(name: &str) -> Self {
118        Self {
119            name: name.to_string(),
120            sql_name: to_snake_case(name),
121            variants: Vec::new(),
122            doc: None,
123        }
124    }
125
126    /// Generate CREATE TYPE SQL.
127    pub fn to_create_type_sql(&self) -> String {
128        let values: Vec<String> = self
129            .variants
130            .iter()
131            .map(|v| format!("'{}'", v.sql_value))
132            .collect();
133
134        format!(
135            "CREATE TYPE {} AS ENUM (\n    {}\n);",
136            self.sql_name,
137            values.join(",\n    ")
138        )
139    }
140}
141
142/// Enum variant definition.
143#[derive(Debug, Clone)]
144pub struct EnumVariant {
145    /// Variant name in Rust.
146    pub name: String,
147
148    /// Value in SQL (lowercase).
149    pub sql_value: String,
150
151    /// Optional integer discriminant value.
152    pub int_value: Option<i32>,
153
154    /// Documentation comment.
155    pub doc: Option<String>,
156}
157
158impl EnumVariant {
159    /// Create a new variant.
160    pub fn new(name: &str) -> Self {
161        Self {
162            name: name.to_string(),
163            sql_value: to_snake_case(name),
164            int_value: None,
165            doc: None,
166        }
167    }
168}
169
170use crate::util::to_snake_case;
171
172#[cfg(test)]
173#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
174mod tests {
175    use super::*;
176    use crate::schema::field::FieldDef;
177    use crate::schema::model::TableDef;
178    use crate::schema::types::RustType;
179
180    #[test]
181    fn test_registry_basic() {
182        let registry = SchemaRegistry::new();
183
184        let mut table = TableDef::new("users", "User");
185        table.fields.push(FieldDef::new("id", RustType::Uuid));
186
187        registry.register_table(table.clone());
188
189        let retrieved = registry.get_table("users").unwrap();
190        assert_eq!(retrieved.name, "users");
191        assert_eq!(retrieved.struct_name, "User");
192    }
193
194    #[test]
195    fn test_enum_def() {
196        let mut enum_def = EnumDef::new("ProjectStatus");
197        enum_def.variants.push(EnumVariant::new("Draft"));
198        enum_def.variants.push(EnumVariant::new("Active"));
199        enum_def.variants.push(EnumVariant::new("Completed"));
200
201        let sql = enum_def.to_create_type_sql();
202        assert!(sql.contains("CREATE TYPE project_status AS ENUM"));
203        assert!(sql.contains("'draft'"));
204        assert!(sql.contains("'active'"));
205    }
206}