scythe_codegen/
overrides.rs1use ahash::AHashSet;
2
3use scythe_core::analyzer::AnalyzedColumn;
4
5#[derive(Debug, Clone)]
14pub struct TypeOverride {
15 pub column: Option<String>,
17 pub db_type: Option<String>,
19 pub neutral_type: Option<String>,
21}
22
23impl TypeOverride {
24 pub fn matches(&self, column_match: &str, col_neutral_type: &str) -> bool {
29 if let Some(ref col) = self.column
30 && col == column_match
31 {
32 return true;
33 }
34 if let Some(ref dt) = self.db_type {
35 return dt.eq_ignore_ascii_case(col_neutral_type);
36 }
37 false
38 }
39}
40
41pub fn column_references<'a>(columns: impl Iterator<Item = &'a AnalyzedColumn>) -> AHashSet<String> {
48 columns
49 .filter_map(|col| {
50 col.source_relation
51 .as_deref()
52 .map(|relation| format!("{relation}.{}", col.name))
53 })
54 .collect()
55}
56
57pub fn unmatched_column_overrides<'a>(
65 overrides: &'a [TypeOverride],
66 known_references: &AHashSet<String>,
67) -> Vec<&'a TypeOverride> {
68 overrides
69 .iter()
70 .filter(|o| matches!(&o.column, Some(c) if !known_references.contains(c)))
71 .collect()
72}
73
74pub fn find_override<'a>(overrides: &'a [TypeOverride], column_match: &str, col_neutral_type: &str) -> Option<&'a str> {
79 overrides.iter().find_map(|o| {
80 if o.matches(column_match, col_neutral_type) {
81 o.neutral_type.as_deref()
82 } else {
83 None
84 }
85 })
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_column_override_matches() {
94 let o = TypeOverride {
95 column: Some("users.metadata".to_string()),
96 db_type: None,
97 neutral_type: Some("json".to_string()),
98 };
99 assert!(o.matches("users.metadata", "jsonb"));
100 assert!(!o.matches("posts.metadata", "jsonb"));
101 }
102
103 #[test]
104 fn test_db_type_override_matches() {
105 let o = TypeOverride {
106 column: None,
107 db_type: Some("ltree".to_string()),
108 neutral_type: Some("string".to_string()),
109 };
110 assert!(o.matches("", "ltree"));
111 assert!(o.matches("any.col", "LTREE"));
112 assert!(!o.matches("any.col", "text"));
113 }
114
115 #[test]
116 fn test_column_takes_priority_over_db_type() {
117 let o = TypeOverride {
118 column: Some("users.name".to_string()),
119 db_type: Some("text".to_string()),
120 neutral_type: Some("custom".to_string()),
121 };
122 assert!(o.matches("users.name", "int32"));
123 assert!(!o.matches("other.name", "int32"));
124 }
125
126 #[test]
131 fn test_column_miss_falls_through_to_db_type() {
132 let o = TypeOverride {
133 column: Some("users.name".to_string()),
134 db_type: Some("text".to_string()),
135 neutral_type: Some("custom".to_string()),
136 };
137 assert!(o.matches("other.name", "text"));
138 assert!(o.matches("other.name", "TEXT"));
139 }
140
141 #[test]
142 fn test_find_override_first_match_wins() {
143 let overrides = vec![
144 TypeOverride {
145 column: Some("users.metadata".to_string()),
146 db_type: None,
147 neutral_type: Some("json".to_string()),
148 },
149 TypeOverride {
150 column: None,
151 db_type: Some("jsonb".to_string()),
152 neutral_type: Some("string".to_string()),
153 },
154 ];
155 assert_eq!(find_override(&overrides, "users.metadata", "jsonb"), Some("json"));
156 assert_eq!(find_override(&overrides, "posts.data", "jsonb"), Some("string"));
157 assert_eq!(find_override(&overrides, "posts.data", "text"), None);
158 }
159
160 #[test]
161 fn test_find_override_empty_list() {
162 assert_eq!(find_override(&[], "users.id", "int32"), None);
163 }
164
165 fn column(name: &str, relation: Option<&str>) -> AnalyzedColumn {
166 AnalyzedColumn {
167 name: name.to_string(),
168 neutral_type: "string".to_string(),
169 source_relation: relation.map(str::to_string),
170 ..Default::default()
171 }
172 }
173
174 #[test]
178 fn test_column_references_skips_columns_with_no_source_relation() {
179 let columns = [column("id", Some("users")), column("total", None)];
180 let refs = column_references(columns.iter());
181 assert_eq!(refs.len(), 1);
182 assert!(refs.contains("users.id"));
183 }
184
185 #[test]
189 fn test_unmatched_column_overrides_reports_only_the_typo() {
190 let known = column_references([column("id", Some("users"))].iter());
191 let overrides = vec![
192 TypeOverride {
193 column: Some("users.id".to_string()),
194 db_type: None,
195 neutral_type: Some("string".to_string()),
196 },
197 TypeOverride {
198 column: Some("users.emial".to_string()),
199 db_type: None,
200 neutral_type: Some("string".to_string()),
201 },
202 TypeOverride {
203 column: None,
204 db_type: Some("jsonb".to_string()),
205 neutral_type: Some("string".to_string()),
206 },
207 ];
208 let unmatched = unmatched_column_overrides(&overrides, &known);
209 assert_eq!(unmatched.len(), 1);
210 assert_eq!(unmatched[0].column.as_deref(), Some("users.emial"));
211 }
212}