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    MultipleDatasource {
55        existing: SourceLoc,
56        incoming: SourceLoc,
57    },
58}
59
60/// Build a SourceLoc from any item that has `source_id: Option<SourceId>` and `span: Span`.
61///
62/// Items whose `source_id` is `None` (i.e., constructed outside the loader,
63/// such as in unit tests) get `SourceId(u32::MAX)`.
64pub(crate) fn loc(source_id: Option<SourceId>, span: Span) -> SourceLoc {
65    SourceLoc::new(source_id.unwrap_or(SourceId(u32::MAX)), span)
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::ast::Schema;
72    use crate::loader::{SourceId, stamp_source};
73    use crate::parser::parse_schema;
74
75    fn stamped(input: &str, sid: u32) -> Schema {
76        let mut s = parse_schema(input).unwrap();
77        stamp_source(&mut s, SourceId(sid));
78        s
79    }
80
81    #[test]
82    fn merge_distinct_models_succeeds() {
83        let mut a = stamped("model A { id Int @id @auto }", 0);
84        let b = stamped("model B { id Int @id @auto }", 1);
85        assert!(a.try_merge(b).is_ok());
86        assert!(a.get_model("A").is_some());
87        assert!(a.get_model("B").is_some());
88        assert_eq!(a.get_model("B").unwrap().source_id, Some(SourceId(1)));
89    }
90
91    #[test]
92    fn merge_duplicate_models_reports_both_locations() {
93        let mut a = stamped("model User { id Int @id @auto }", 0);
94        let b = stamped("model User { id Int @id @auto }", 1);
95        let err = a.try_merge(b).unwrap_err();
96        assert_eq!(err.len(), 1);
97        match &err[0] {
98            MergeConflict::DuplicateModel {
99                name,
100                existing,
101                incoming,
102            } => {
103                assert_eq!(name.as_str(), "User");
104                assert_eq!(existing.source, SourceId(0));
105                assert_eq!(incoming.source, SourceId(1));
106            }
107            other => panic!("unexpected conflict: {other:?}"),
108        }
109    }
110
111    #[test]
112    fn merge_collects_all_conflicts_without_short_circuit() {
113        let mut a = stamped(
114            "model A { id Int @id @auto } model B { id Int @id @auto }",
115            0,
116        );
117        let b = stamped(
118            "model A { id Int @id @auto } model B { id Int @id @auto }",
119            1,
120        );
121        let err = a.try_merge(b).unwrap_err();
122        assert_eq!(err.len(), 2);
123    }
124
125    #[test]
126    fn merge_two_datasources_errors() {
127        let mut a = stamped(r#"datasource db { provider = "postgresql" url = "x" }"#, 0);
128        let b = stamped(r#"datasource db { provider = "postgresql" url = "y" }"#, 1);
129        let err = a.try_merge(b).unwrap_err();
130        assert!(matches!(err[0], MergeConflict::MultipleDatasource { .. }));
131    }
132}