Skip to main content

prax_schema/loader/
merge.rs

1//! Schema merging with cross-file collision detection.
2
3use smol_str::SmolStr;
4
5use super::source::{SourceId, SourceLoc};
6use crate::ast::Span;
7
8/// A single conflict found while merging two [`Schema`](crate::ast::Schema)s.
9///
10/// Collected without short-circuiting so the loader can report every duplicate
11/// in one pass.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum MergeConflict {
14    DuplicateModel {
15        name: SmolStr,
16        existing: SourceLoc,
17        incoming: SourceLoc,
18    },
19    DuplicateEnum {
20        name: SmolStr,
21        existing: SourceLoc,
22        incoming: SourceLoc,
23    },
24    DuplicateType {
25        name: SmolStr,
26        existing: SourceLoc,
27        incoming: SourceLoc,
28    },
29    DuplicateView {
30        name: SmolStr,
31        existing: SourceLoc,
32        incoming: SourceLoc,
33    },
34    DuplicateServerGroup {
35        name: SmolStr,
36        existing: SourceLoc,
37        incoming: SourceLoc,
38    },
39    DuplicatePolicy {
40        name: SmolStr,
41        existing: SourceLoc,
42        incoming: SourceLoc,
43    },
44    DuplicateGenerator {
45        name: SmolStr,
46        existing: SourceLoc,
47        incoming: SourceLoc,
48    },
49    DuplicateRawSql {
50        name: SmolStr,
51        existing: SourceLoc,
52        incoming: SourceLoc,
53    },
54    DuplicateProcedure {
55        name: SmolStr,
56        existing: SourceLoc,
57        incoming: SourceLoc,
58    },
59    MultipleDatasource {
60        existing: SourceLoc,
61        incoming: SourceLoc,
62    },
63}
64
65/// Build a SourceLoc from any item that has `source_id: Option<SourceId>` and `span: Span`.
66///
67/// Items whose `source_id` is `None` (i.e., constructed outside the loader,
68/// such as in unit tests) get `SourceId(u32::MAX)`.
69pub(crate) fn loc(source_id: Option<SourceId>, span: Span) -> SourceLoc {
70    SourceLoc::new(source_id.unwrap_or(SourceId(u32::MAX)), span)
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::ast::Schema;
77    use crate::loader::{SourceId, stamp_source};
78    use crate::parser::parse_schema;
79
80    fn stamped(input: &str, sid: u32) -> Schema {
81        let mut s = parse_schema(input).unwrap();
82        stamp_source(&mut s, SourceId(sid));
83        s
84    }
85
86    #[test]
87    fn merge_distinct_models_succeeds() {
88        let mut a = stamped("model A { id Int @id @auto }", 0);
89        let b = stamped("model B { id Int @id @auto }", 1);
90        assert!(a.try_merge(b).is_ok());
91        assert!(a.get_model("A").is_some());
92        assert!(a.get_model("B").is_some());
93        assert_eq!(a.get_model("B").unwrap().source_id, Some(SourceId(1)));
94    }
95
96    #[test]
97    fn merge_duplicate_models_reports_both_locations() {
98        let mut a = stamped("model User { id Int @id @auto }", 0);
99        let b = stamped("model User { id Int @id @auto }", 1);
100        let err = a.try_merge(b).unwrap_err();
101        assert_eq!(err.len(), 1);
102        match &err[0] {
103            MergeConflict::DuplicateModel {
104                name,
105                existing,
106                incoming,
107            } => {
108                assert_eq!(name.as_str(), "User");
109                assert_eq!(existing.source, SourceId(0));
110                assert_eq!(incoming.source, SourceId(1));
111            }
112            other => panic!("unexpected conflict: {other:?}"),
113        }
114    }
115
116    #[test]
117    fn merge_collects_all_conflicts_without_short_circuit() {
118        let mut a = stamped(
119            "model A { id Int @id @auto } model B { id Int @id @auto }",
120            0,
121        );
122        let b = stamped(
123            "model A { id Int @id @auto } model B { id Int @id @auto }",
124            1,
125        );
126        let err = a.try_merge(b).unwrap_err();
127        assert_eq!(err.len(), 2);
128    }
129
130    #[test]
131    fn merge_two_datasources_errors() {
132        let mut a = stamped(r#"datasource db { provider = "postgresql" url = "x" }"#, 0);
133        let b = stamped(r#"datasource db { provider = "postgresql" url = "y" }"#, 1);
134        let err = a.try_merge(b).unwrap_err();
135        assert!(matches!(err[0], MergeConflict::MultipleDatasource { .. }));
136    }
137}