Skip to main content

prax_schema/ast/
schema.rs

1//! Top-level schema definition.
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use smol_str::SmolStr;
6
7use super::{
8    CompositeType, Datasource, Enum, Generator, Model, Policy, Relation, ServerGroup, View,
9};
10
11/// A complete Prax schema.
12#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
13pub struct Schema {
14    /// Datasource configuration (database connection and extensions).
15    pub datasource: Option<Datasource>,
16    /// Generator configurations.
17    pub generators: IndexMap<SmolStr, Generator>,
18    /// All models in the schema.
19    pub models: IndexMap<SmolStr, Model>,
20    /// All enums in the schema.
21    pub enums: IndexMap<SmolStr, Enum>,
22    /// All composite types in the schema.
23    pub types: IndexMap<SmolStr, CompositeType>,
24    /// All views in the schema.
25    pub views: IndexMap<SmolStr, View>,
26    /// Server groups for multi-server configurations.
27    pub server_groups: IndexMap<SmolStr, ServerGroup>,
28    /// PostgreSQL Row-Level Security policies.
29    pub policies: Vec<Policy>,
30    /// Raw SQL definitions.
31    pub raw_sql: Vec<RawSql>,
32    /// Resolved relations (populated after validation).
33    pub relations: Vec<Relation>,
34}
35
36impl Schema {
37    /// Create a new empty schema.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Set the datasource configuration.
43    pub fn set_datasource(&mut self, datasource: Datasource) {
44        self.datasource = Some(datasource);
45    }
46
47    /// Get the datasource configuration.
48    pub fn datasource(&self) -> Option<&Datasource> {
49        self.datasource.as_ref()
50    }
51
52    /// Check if the schema has vector extension enabled.
53    pub fn has_vector_support(&self) -> bool {
54        self.datasource
55            .as_ref()
56            .is_some_and(|ds| ds.has_vector_support())
57    }
58
59    /// Get all required PostgreSQL extensions from the datasource.
60    pub fn required_extensions(&self) -> Vec<&super::PostgresExtension> {
61        self.datasource
62            .as_ref()
63            .map(|ds| ds.extensions.iter().collect())
64            .unwrap_or_default()
65    }
66
67    /// Add a generator to the schema.
68    pub fn add_generator(&mut self, generator: Generator) {
69        self.generators.insert(generator.name.clone(), generator);
70    }
71
72    /// Get a generator by name.
73    pub fn get_generator(&self, name: &str) -> Option<&Generator> {
74        self.generators.get(name)
75    }
76
77    /// Get all enabled generators.
78    pub fn enabled_generators(&self) -> Vec<&Generator> {
79        self.generators
80            .values()
81            .filter(|g| g.is_enabled())
82            .collect()
83    }
84
85    /// Add a model to the schema.
86    pub fn add_model(&mut self, model: Model) {
87        self.models.insert(model.name.name.clone(), model);
88    }
89
90    /// Add an enum to the schema.
91    pub fn add_enum(&mut self, e: Enum) {
92        self.enums.insert(e.name.name.clone(), e);
93    }
94
95    /// Add a composite type to the schema.
96    pub fn add_type(&mut self, t: CompositeType) {
97        self.types.insert(t.name.name.clone(), t);
98    }
99
100    /// Add a view to the schema.
101    pub fn add_view(&mut self, v: View) {
102        self.views.insert(v.name.name.clone(), v);
103    }
104
105    /// Add a server group to the schema.
106    pub fn add_server_group(&mut self, sg: ServerGroup) {
107        self.server_groups.insert(sg.name.name.clone(), sg);
108    }
109
110    /// Add a PostgreSQL Row-Level Security policy.
111    pub fn add_policy(&mut self, policy: Policy) {
112        self.policies.push(policy);
113    }
114
115    /// Add a raw SQL definition.
116    pub fn add_raw_sql(&mut self, sql: RawSql) {
117        self.raw_sql.push(sql);
118    }
119
120    /// Get a model by name.
121    pub fn get_model(&self, name: &str) -> Option<&Model> {
122        self.models.get(name)
123    }
124
125    /// Get a mutable model by name.
126    pub fn get_model_mut(&mut self, name: &str) -> Option<&mut Model> {
127        self.models.get_mut(name)
128    }
129
130    /// Get an enum by name.
131    pub fn get_enum(&self, name: &str) -> Option<&Enum> {
132        self.enums.get(name)
133    }
134
135    /// Get a composite type by name.
136    pub fn get_type(&self, name: &str) -> Option<&CompositeType> {
137        self.types.get(name)
138    }
139
140    /// Get a view by name.
141    pub fn get_view(&self, name: &str) -> Option<&View> {
142        self.views.get(name)
143    }
144
145    /// Get a server group by name.
146    pub fn get_server_group(&self, name: &str) -> Option<&ServerGroup> {
147        self.server_groups.get(name)
148    }
149
150    /// Get all server group names.
151    pub fn server_group_names(&self) -> impl Iterator<Item = &str> {
152        self.server_groups.keys().map(|s| s.as_str())
153    }
154
155    /// Get a policy by name.
156    pub fn get_policy(&self, name: &str) -> Option<&Policy> {
157        self.policies.iter().find(|p| p.name() == name)
158    }
159
160    /// Get all policies for a specific model/table.
161    pub fn policies_for(&self, model: &str) -> Vec<&Policy> {
162        self.policies
163            .iter()
164            .filter(|p| p.table() == model)
165            .collect()
166    }
167
168    /// Check if a model has Row-Level Security policies.
169    pub fn has_policies(&self, model: &str) -> bool {
170        self.policies.iter().any(|p| p.table() == model)
171    }
172
173    /// Get all policy names.
174    pub fn policy_names(&self) -> impl Iterator<Item = &str> {
175        self.policies.iter().map(|p| p.name())
176    }
177
178    /// Check if a type name exists (model, enum, type, or view).
179    pub fn type_exists(&self, name: &str) -> bool {
180        self.models.contains_key(name)
181            || self.enums.contains_key(name)
182            || self.types.contains_key(name)
183            || self.views.contains_key(name)
184    }
185
186    /// Get all model names.
187    pub fn model_names(&self) -> impl Iterator<Item = &str> {
188        self.models.keys().map(|s| s.as_str())
189    }
190
191    /// Get all enum names.
192    pub fn enum_names(&self) -> impl Iterator<Item = &str> {
193        self.enums.keys().map(|s| s.as_str())
194    }
195
196    /// Get relations for a specific model.
197    pub fn relations_for(&self, model: &str) -> Vec<&Relation> {
198        self.relations
199            .iter()
200            .filter(|r| r.from_model == model || r.to_model == model)
201            .collect()
202    }
203
204    /// Get relations originating from a specific model.
205    pub fn relations_from(&self, model: &str) -> Vec<&Relation> {
206        self.relations
207            .iter()
208            .filter(|r| r.from_model == model)
209            .collect()
210    }
211
212    /// Merge another schema into this one (deprecated: use [`Schema::try_merge`]).
213    ///
214    /// Silently overwrites duplicates. Kept for backward compatibility; will be
215    /// removed in a future release.
216    #[deprecated(since = "0.9.8", note = "use try_merge for collision-aware merging")]
217    pub fn merge(&mut self, other: Schema) {
218        self.models.extend(other.models);
219        self.enums.extend(other.enums);
220        self.types.extend(other.types);
221        self.views.extend(other.views);
222        self.server_groups.extend(other.server_groups);
223        self.policies.extend(other.policies);
224        self.raw_sql.extend(other.raw_sql);
225    }
226
227    /// Merge `other` into `self`, returning every collision found rather than
228    /// silently overwriting.
229    ///
230    /// Items whose `source_id` is `None` (built outside the loader, e.g. in
231    /// tests) are reported with `SourceId(u32::MAX)`. In production usage the
232    /// loader stamps every item via [`crate::loader::stamp_source`] first.
233    pub fn try_merge(&mut self, other: Schema) -> Result<(), Vec<crate::loader::MergeConflict>> {
234        use crate::loader::MergeConflict;
235        use crate::loader::merge::loc;
236
237        let mut conflicts: Vec<MergeConflict> = Vec::new();
238
239        for (name, m) in other.models {
240            if let Some(existing) = self.models.get(&name) {
241                conflicts.push(MergeConflict::DuplicateModel {
242                    name: name.clone(),
243                    existing: loc(existing.source_id, existing.span),
244                    incoming: loc(m.source_id, m.span),
245                });
246            } else {
247                self.models.insert(name, m);
248            }
249        }
250
251        for (name, e) in other.enums {
252            if let Some(existing) = self.enums.get(&name) {
253                conflicts.push(MergeConflict::DuplicateEnum {
254                    name: name.clone(),
255                    existing: loc(existing.source_id, existing.span),
256                    incoming: loc(e.source_id, e.span),
257                });
258            } else {
259                self.enums.insert(name, e);
260            }
261        }
262
263        for (name, t) in other.types {
264            if let Some(existing) = self.types.get(&name) {
265                conflicts.push(MergeConflict::DuplicateType {
266                    name: name.clone(),
267                    existing: loc(existing.source_id, existing.span),
268                    incoming: loc(t.source_id, t.span),
269                });
270            } else {
271                self.types.insert(name, t);
272            }
273        }
274
275        for (name, v) in other.views {
276            if let Some(existing) = self.views.get(&name) {
277                conflicts.push(MergeConflict::DuplicateView {
278                    name: name.clone(),
279                    existing: loc(existing.source_id, existing.span),
280                    incoming: loc(v.source_id, v.span),
281                });
282            } else {
283                self.views.insert(name, v);
284            }
285        }
286
287        for (name, sg) in other.server_groups {
288            if let Some(existing) = self.server_groups.get(&name) {
289                conflicts.push(MergeConflict::DuplicateServerGroup {
290                    name: name.clone(),
291                    existing: loc(existing.source_id, existing.span),
292                    incoming: loc(sg.source_id, sg.span),
293                });
294            } else {
295                self.server_groups.insert(name, sg);
296            }
297        }
298
299        for (name, g) in other.generators {
300            if let Some(existing) = self.generators.get(&name) {
301                conflicts.push(MergeConflict::DuplicateGenerator {
302                    name: name.clone(),
303                    existing: loc(existing.source_id, existing.span),
304                    incoming: loc(g.source_id, g.span),
305                });
306            } else {
307                self.generators.insert(name, g);
308            }
309        }
310
311        for p in other.policies {
312            if let Some(existing) = self.policies.iter().find(|x| x.name() == p.name()) {
313                conflicts.push(MergeConflict::DuplicatePolicy {
314                    name: SmolStr::new(p.name()),
315                    existing: loc(existing.source_id, existing.span),
316                    incoming: loc(p.source_id, p.span),
317                });
318            } else {
319                self.policies.push(p);
320            }
321        }
322
323        for r in other.raw_sql {
324            if let Some(existing) = self.raw_sql.iter().find(|x| x.name == r.name) {
325                conflicts.push(MergeConflict::DuplicateRawSql {
326                    name: r.name.clone(),
327                    existing: loc(existing.source_id, super::Span::new(0, 0)),
328                    incoming: loc(r.source_id, super::Span::new(0, 0)),
329                });
330            } else {
331                self.raw_sql.push(r);
332            }
333        }
334
335        match (&self.datasource, other.datasource) {
336            (Some(existing), Some(incoming)) => {
337                conflicts.push(MergeConflict::MultipleDatasource {
338                    existing: loc(existing.source_id, existing.span),
339                    incoming: loc(incoming.source_id, incoming.span),
340                });
341            }
342            (None, Some(incoming)) => self.datasource = Some(incoming),
343            (_, None) => {}
344        }
345
346        if conflicts.is_empty() {
347            Ok(())
348        } else {
349            Err(conflicts)
350        }
351    }
352}
353
354/// A raw SQL definition.
355#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
356pub struct RawSql {
357    /// Name/identifier for the SQL (e.g., view name).
358    pub name: SmolStr,
359    /// The raw SQL content.
360    pub sql: String,
361    /// Source file this raw SQL was parsed from (None for single-file path).
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub source_id: Option<crate::loader::SourceId>,
364}
365
366impl RawSql {
367    /// Create a new raw SQL definition.
368    pub fn new(name: impl Into<SmolStr>, sql: impl Into<String>) -> Self {
369        Self {
370            name: name.into(),
371            sql: sql.into(),
372            source_id: None,
373        }
374    }
375}
376
377/// Schema statistics for debugging/info.
378#[derive(Debug, Clone, Default)]
379pub struct SchemaStats {
380    /// Number of models.
381    pub model_count: usize,
382    /// Number of enums.
383    pub enum_count: usize,
384    /// Number of composite types.
385    pub type_count: usize,
386    /// Number of views.
387    pub view_count: usize,
388    /// Number of server groups.
389    pub server_group_count: usize,
390    /// Number of RLS policies.
391    pub policy_count: usize,
392    /// Total number of fields across all models.
393    pub field_count: usize,
394    /// Number of relations.
395    pub relation_count: usize,
396}
397
398impl Schema {
399    /// Get statistics about the schema.
400    pub fn stats(&self) -> SchemaStats {
401        SchemaStats {
402            model_count: self.models.len(),
403            enum_count: self.enums.len(),
404            type_count: self.types.len(),
405            view_count: self.views.len(),
406            server_group_count: self.server_groups.len(),
407            policy_count: self.policies.len(),
408            field_count: self.models.values().map(|m| m.fields.len()).sum(),
409            relation_count: self.relations.len(),
410        }
411    }
412}
413
414impl std::fmt::Display for Schema {
415    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416        let stats = self.stats();
417        write!(
418            f,
419            "Schema({} models, {} enums, {} types, {} views, {} server groups, {} policies, {} fields, {} relations)",
420            stats.model_count,
421            stats.enum_count,
422            stats.type_count,
423            stats.view_count,
424            stats.server_group_count,
425            stats.policy_count,
426            stats.field_count,
427            stats.relation_count
428        )
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use crate::ast::{
436        Attribute, EnumVariant, Field, FieldType, Ident, Policy, RelationType, ScalarType, Span,
437        TypeModifier,
438    };
439
440    fn make_span() -> Span {
441        Span::new(0, 10)
442    }
443
444    fn make_ident(name: &str) -> Ident {
445        Ident::new(name, make_span())
446    }
447
448    fn make_model(name: &str) -> Model {
449        let mut model = Model::new(make_ident(name), make_span());
450        let id_field = make_id_field();
451        model.add_field(id_field);
452        model
453    }
454
455    fn make_id_field() -> Field {
456        let mut field = Field::new(
457            make_ident("id"),
458            FieldType::Scalar(ScalarType::Int),
459            TypeModifier::Required,
460            vec![],
461            make_span(),
462        );
463        field
464            .attributes
465            .push(Attribute::simple(make_ident("id"), make_span()));
466        field
467    }
468
469    fn make_field(name: &str, field_type: FieldType) -> Field {
470        Field::new(
471            make_ident(name),
472            field_type,
473            TypeModifier::Required,
474            vec![],
475            make_span(),
476        )
477    }
478
479    fn make_enum(name: &str, variants: &[&str]) -> Enum {
480        let mut e = Enum::new(make_ident(name), make_span());
481        for v in variants {
482            e.add_variant(EnumVariant::new(make_ident(v), make_span()));
483        }
484        e
485    }
486
487    // ==================== Schema Tests ====================
488
489    #[test]
490    fn test_schema_new() {
491        let schema = Schema::new();
492        assert!(schema.models.is_empty());
493        assert!(schema.enums.is_empty());
494        assert!(schema.types.is_empty());
495        assert!(schema.views.is_empty());
496        assert!(schema.policies.is_empty());
497        assert!(schema.raw_sql.is_empty());
498        assert!(schema.relations.is_empty());
499    }
500
501    #[test]
502    fn test_schema_default() {
503        let schema = Schema::default();
504        assert!(schema.models.is_empty());
505    }
506
507    #[test]
508    fn test_schema_add_model() {
509        let mut schema = Schema::new();
510        let model = make_model("User");
511
512        schema.add_model(model);
513
514        assert_eq!(schema.models.len(), 1);
515        assert!(schema.models.contains_key("User"));
516    }
517
518    #[test]
519    fn test_schema_add_multiple_models() {
520        let mut schema = Schema::new();
521        schema.add_model(make_model("User"));
522        schema.add_model(make_model("Post"));
523        schema.add_model(make_model("Comment"));
524
525        assert_eq!(schema.models.len(), 3);
526    }
527
528    #[test]
529    fn test_schema_add_enum() {
530        let mut schema = Schema::new();
531        let e = make_enum("Role", &["User", "Admin"]);
532
533        schema.add_enum(e);
534
535        assert_eq!(schema.enums.len(), 1);
536        assert!(schema.enums.contains_key("Role"));
537    }
538
539    #[test]
540    fn test_schema_add_type() {
541        let mut schema = Schema::new();
542        let ct = CompositeType::new(make_ident("Address"), make_span());
543
544        schema.add_type(ct);
545
546        assert_eq!(schema.types.len(), 1);
547        assert!(schema.types.contains_key("Address"));
548    }
549
550    #[test]
551    fn test_schema_add_view() {
552        let mut schema = Schema::new();
553        let view = View::new(make_ident("UserStats"), make_span());
554
555        schema.add_view(view);
556
557        assert_eq!(schema.views.len(), 1);
558        assert!(schema.views.contains_key("UserStats"));
559    }
560
561    #[test]
562    fn test_schema_add_raw_sql() {
563        let mut schema = Schema::new();
564        let sql = RawSql::new("migration_1", "CREATE TABLE test ();");
565
566        schema.add_raw_sql(sql);
567
568        assert_eq!(schema.raw_sql.len(), 1);
569    }
570
571    #[test]
572    fn test_schema_get_model() {
573        let mut schema = Schema::new();
574        schema.add_model(make_model("User"));
575
576        let model = schema.get_model("User");
577        assert!(model.is_some());
578        assert_eq!(model.unwrap().name(), "User");
579
580        assert!(schema.get_model("NonExistent").is_none());
581    }
582
583    #[test]
584    fn test_schema_get_model_mut() {
585        let mut schema = Schema::new();
586        schema.add_model(make_model("User"));
587
588        let model = schema.get_model_mut("User");
589        assert!(model.is_some());
590
591        // Modify the model
592        let model = model.unwrap();
593        model.add_field(make_field("email", FieldType::Scalar(ScalarType::String)));
594
595        // Verify modification persisted
596        assert_eq!(schema.get_model("User").unwrap().fields.len(), 2);
597    }
598
599    #[test]
600    fn test_schema_get_enum() {
601        let mut schema = Schema::new();
602        schema.add_enum(make_enum("Role", &["User", "Admin"]));
603
604        let e = schema.get_enum("Role");
605        assert!(e.is_some());
606        assert_eq!(e.unwrap().name(), "Role");
607
608        assert!(schema.get_enum("NonExistent").is_none());
609    }
610
611    #[test]
612    fn test_schema_get_type() {
613        let mut schema = Schema::new();
614        schema.add_type(CompositeType::new(make_ident("Address"), make_span()));
615
616        let ct = schema.get_type("Address");
617        assert!(ct.is_some());
618
619        assert!(schema.get_type("NonExistent").is_none());
620    }
621
622    #[test]
623    fn test_schema_get_view() {
624        let mut schema = Schema::new();
625        schema.add_view(View::new(make_ident("Stats"), make_span()));
626
627        let v = schema.get_view("Stats");
628        assert!(v.is_some());
629
630        assert!(schema.get_view("NonExistent").is_none());
631    }
632
633    #[test]
634    fn test_schema_type_exists() {
635        let mut schema = Schema::new();
636        schema.add_model(make_model("User"));
637        schema.add_enum(make_enum("Role", &["User"]));
638        schema.add_type(CompositeType::new(make_ident("Address"), make_span()));
639        schema.add_view(View::new(make_ident("Stats"), make_span()));
640
641        assert!(schema.type_exists("User")); // model
642        assert!(schema.type_exists("Role")); // enum
643        assert!(schema.type_exists("Address")); // type
644        assert!(schema.type_exists("Stats")); // view
645        assert!(!schema.type_exists("NonExistent"));
646    }
647
648    #[test]
649    fn test_schema_model_names() {
650        let mut schema = Schema::new();
651        schema.add_model(make_model("User"));
652        schema.add_model(make_model("Post"));
653
654        let names: Vec<_> = schema.model_names().collect();
655        assert_eq!(names.len(), 2);
656        assert!(names.contains(&"User"));
657        assert!(names.contains(&"Post"));
658    }
659
660    #[test]
661    fn test_schema_enum_names() {
662        let mut schema = Schema::new();
663        schema.add_enum(make_enum("Role", &["User"]));
664        schema.add_enum(make_enum("Status", &["Active"]));
665
666        let names: Vec<_> = schema.enum_names().collect();
667        assert_eq!(names.len(), 2);
668        assert!(names.contains(&"Role"));
669        assert!(names.contains(&"Status"));
670    }
671
672    #[test]
673    fn test_schema_relations_for() {
674        let mut schema = Schema::new();
675        schema.relations.push(Relation::new(
676            "Post",
677            "author",
678            "User",
679            RelationType::ManyToOne,
680        ));
681        schema.relations.push(Relation::new(
682            "Comment",
683            "user",
684            "User",
685            RelationType::ManyToOne,
686        ));
687        schema.relations.push(Relation::new(
688            "Post",
689            "tags",
690            "Tag",
691            RelationType::ManyToMany,
692        ));
693
694        let user_relations = schema.relations_for("User");
695        assert_eq!(user_relations.len(), 2);
696
697        let post_relations = schema.relations_for("Post");
698        assert_eq!(post_relations.len(), 2);
699
700        let tag_relations = schema.relations_for("Tag");
701        assert_eq!(tag_relations.len(), 1);
702    }
703
704    #[test]
705    fn test_schema_relations_from() {
706        let mut schema = Schema::new();
707        schema.relations.push(Relation::new(
708            "Post",
709            "author",
710            "User",
711            RelationType::ManyToOne,
712        ));
713        schema.relations.push(Relation::new(
714            "Post",
715            "tags",
716            "Tag",
717            RelationType::ManyToMany,
718        ));
719        schema.relations.push(Relation::new(
720            "User",
721            "posts",
722            "Post",
723            RelationType::OneToMany,
724        ));
725
726        let post_relations = schema.relations_from("Post");
727        assert_eq!(post_relations.len(), 2);
728
729        let user_relations = schema.relations_from("User");
730        assert_eq!(user_relations.len(), 1);
731
732        let tag_relations = schema.relations_from("Tag");
733        assert_eq!(tag_relations.len(), 0);
734    }
735
736    #[test]
737    #[allow(deprecated)]
738    fn test_schema_merge() {
739        let mut schema1 = Schema::new();
740        schema1.add_model(make_model("User"));
741        schema1.add_enum(make_enum("Role", &["User"]));
742
743        let mut schema2 = Schema::new();
744        schema2.add_model(make_model("Post"));
745        schema2.add_enum(make_enum("Status", &["Active"]));
746        schema2.add_raw_sql(RawSql::new("init", "-- init"));
747
748        schema1.merge(schema2);
749
750        assert_eq!(schema1.models.len(), 2);
751        assert_eq!(schema1.enums.len(), 2);
752        assert_eq!(schema1.raw_sql.len(), 1);
753    }
754
755    #[test]
756    fn test_schema_stats() {
757        let mut schema = Schema::new();
758
759        let mut user = make_model("User");
760        user.add_field(make_field("email", FieldType::Scalar(ScalarType::String)));
761        user.add_field(make_field("name", FieldType::Scalar(ScalarType::String)));
762        schema.add_model(user);
763
764        let mut post = make_model("Post");
765        post.add_field(make_field("title", FieldType::Scalar(ScalarType::String)));
766        schema.add_model(post);
767
768        schema.add_enum(make_enum("Role", &["User", "Admin"]));
769        schema.add_type(CompositeType::new(make_ident("Address"), make_span()));
770        schema.add_view(View::new(make_ident("Stats"), make_span()));
771        schema.relations.push(Relation::new(
772            "Post",
773            "author",
774            "User",
775            RelationType::ManyToOne,
776        ));
777
778        let stats = schema.stats();
779        assert_eq!(stats.model_count, 2);
780        assert_eq!(stats.enum_count, 1);
781        assert_eq!(stats.type_count, 1);
782        assert_eq!(stats.view_count, 1);
783        assert_eq!(stats.field_count, 5); // 3 in User + 2 in Post
784        assert_eq!(stats.relation_count, 1);
785    }
786
787    #[test]
788    fn test_schema_display() {
789        let mut schema = Schema::new();
790        schema.add_model(make_model("User"));
791        schema.add_enum(make_enum("Role", &["User"]));
792
793        let display = format!("{}", schema);
794        assert!(display.contains("1 models"));
795        assert!(display.contains("1 enums"));
796        assert!(display.contains("0 policies"));
797    }
798
799    #[test]
800    fn test_schema_equality() {
801        let schema1 = Schema::new();
802        let schema2 = Schema::new();
803        assert_eq!(schema1, schema2);
804    }
805
806    #[test]
807    fn test_schema_clone() {
808        let mut schema = Schema::new();
809        schema.add_model(make_model("User"));
810
811        let cloned = schema.clone();
812        assert_eq!(cloned.models.len(), 1);
813    }
814
815    // ==================== RawSql Tests ====================
816
817    #[test]
818    fn test_raw_sql_new() {
819        let sql = RawSql::new("create_users", "CREATE TABLE users ();");
820
821        assert_eq!(sql.name.as_str(), "create_users");
822        assert_eq!(sql.sql, "CREATE TABLE users ();");
823    }
824
825    #[test]
826    fn test_raw_sql_from_strings() {
827        let name = String::from("migration");
828        let content = String::from("ALTER TABLE users ADD COLUMN age INT;");
829        let sql = RawSql::new(name, content);
830
831        assert_eq!(sql.name.as_str(), "migration");
832    }
833
834    #[test]
835    fn test_raw_sql_equality() {
836        let sql1 = RawSql::new("test", "SELECT 1;");
837        let sql2 = RawSql::new("test", "SELECT 1;");
838        let sql3 = RawSql::new("test", "SELECT 2;");
839
840        assert_eq!(sql1, sql2);
841        assert_ne!(sql1, sql3);
842    }
843
844    #[test]
845    fn test_raw_sql_clone() {
846        let sql = RawSql::new("test", "SELECT 1;");
847        let cloned = sql.clone();
848        assert_eq!(sql, cloned);
849    }
850
851    // ==================== SchemaStats Tests ====================
852
853    #[test]
854    fn test_schema_stats_default() {
855        let stats = SchemaStats::default();
856        assert_eq!(stats.model_count, 0);
857        assert_eq!(stats.enum_count, 0);
858        assert_eq!(stats.type_count, 0);
859        assert_eq!(stats.view_count, 0);
860        assert_eq!(stats.policy_count, 0);
861        assert_eq!(stats.field_count, 0);
862        assert_eq!(stats.relation_count, 0);
863    }
864
865    #[test]
866    fn test_schema_stats_debug() {
867        let stats = SchemaStats::default();
868        let debug = format!("{:?}", stats);
869        assert!(debug.contains("SchemaStats"));
870    }
871
872    #[test]
873    fn test_schema_stats_clone() {
874        let stats = SchemaStats {
875            model_count: 5,
876            enum_count: 2,
877            type_count: 1,
878            view_count: 3,
879            server_group_count: 2,
880            policy_count: 4,
881            field_count: 25,
882            relation_count: 10,
883        };
884        let cloned = stats.clone();
885        assert_eq!(cloned.model_count, 5);
886        assert_eq!(cloned.field_count, 25);
887        assert_eq!(cloned.policy_count, 4);
888    }
889
890    // ==================== Policy Schema Tests ====================
891
892    #[test]
893    fn test_schema_add_policy() {
894        let mut schema = Schema::new();
895        let policy = Policy::new(make_ident("read_own"), make_ident("User"), make_span());
896
897        schema.add_policy(policy);
898
899        assert_eq!(schema.policies.len(), 1);
900    }
901
902    #[test]
903    fn test_schema_get_policy() {
904        let mut schema = Schema::new();
905        schema.add_policy(Policy::new(
906            make_ident("read_own"),
907            make_ident("User"),
908            make_span(),
909        ));
910
911        let policy = schema.get_policy("read_own");
912        assert!(policy.is_some());
913        assert_eq!(policy.unwrap().name(), "read_own");
914
915        assert!(schema.get_policy("nonexistent").is_none());
916    }
917
918    #[test]
919    fn test_schema_policies_for_model() {
920        let mut schema = Schema::new();
921        schema.add_policy(Policy::new(
922            make_ident("user_read"),
923            make_ident("User"),
924            make_span(),
925        ));
926        schema.add_policy(Policy::new(
927            make_ident("user_write"),
928            make_ident("User"),
929            make_span(),
930        ));
931        schema.add_policy(Policy::new(
932            make_ident("post_read"),
933            make_ident("Post"),
934            make_span(),
935        ));
936
937        let user_policies = schema.policies_for("User");
938        assert_eq!(user_policies.len(), 2);
939
940        let post_policies = schema.policies_for("Post");
941        assert_eq!(post_policies.len(), 1);
942
943        let comment_policies = schema.policies_for("Comment");
944        assert!(comment_policies.is_empty());
945    }
946
947    #[test]
948    fn test_schema_has_policies() {
949        let mut schema = Schema::new();
950        schema.add_policy(Policy::new(
951            make_ident("test"),
952            make_ident("User"),
953            make_span(),
954        ));
955
956        assert!(schema.has_policies("User"));
957        assert!(!schema.has_policies("Post"));
958    }
959
960    #[test]
961    fn test_schema_policy_names() {
962        let mut schema = Schema::new();
963        schema.add_policy(Policy::new(
964            make_ident("policy1"),
965            make_ident("User"),
966            make_span(),
967        ));
968        schema.add_policy(Policy::new(
969            make_ident("policy2"),
970            make_ident("Post"),
971            make_span(),
972        ));
973
974        let names: Vec<_> = schema.policy_names().collect();
975        assert_eq!(names.len(), 2);
976        assert!(names.contains(&"policy1"));
977        assert!(names.contains(&"policy2"));
978    }
979
980    #[test]
981    #[allow(deprecated)]
982    fn test_schema_merge_with_policies() {
983        let mut schema1 = Schema::new();
984        schema1.add_policy(Policy::new(
985            make_ident("policy1"),
986            make_ident("User"),
987            make_span(),
988        ));
989
990        let mut schema2 = Schema::new();
991        schema2.add_policy(Policy::new(
992            make_ident("policy2"),
993            make_ident("Post"),
994            make_span(),
995        ));
996
997        schema1.merge(schema2);
998
999        assert_eq!(schema1.policies.len(), 2);
1000    }
1001
1002    #[test]
1003    fn test_schema_stats_with_policies() {
1004        let mut schema = Schema::new();
1005        schema.add_model(make_model("User"));
1006        schema.add_policy(Policy::new(
1007            make_ident("policy1"),
1008            make_ident("User"),
1009            make_span(),
1010        ));
1011        schema.add_policy(Policy::new(
1012            make_ident("policy2"),
1013            make_ident("User"),
1014            make_span(),
1015        ));
1016
1017        let stats = schema.stats();
1018        assert_eq!(stats.model_count, 1);
1019        assert_eq!(stats.policy_count, 2);
1020    }
1021}