Skip to main content

scythe_codegen/
overrides.rs

1/// A type override that replaces the inferred neutral type for a column or SQL type.
2///
3/// Overrides are evaluated in order: the first match wins. A `column` match
4/// (e.g. `"users.metadata"`) takes priority over a `db_type` match when both
5/// fields are set on the same override entry.
6#[derive(Debug, Clone)]
7pub struct TypeOverride {
8    /// Fully-qualified column reference in `"table.column"` format.
9    pub column: Option<String>,
10    /// SQL type name (matched case-insensitively against the column's neutral type).
11    pub db_type: Option<String>,
12    /// Target neutral type to substitute (e.g. `"string"`, `"json"`).
13    pub neutral_type: Option<String>,
14}
15
16impl TypeOverride {
17    /// Check if this override matches a column.
18    ///
19    /// `column_match` is `"table_name.column_name"` (empty string if unknown).
20    /// `col_neutral_type` is the neutral type inferred by the analyzer.
21    pub fn matches(&self, column_match: &str, col_neutral_type: &str) -> bool {
22        if let Some(ref col) = self.column {
23            return col == column_match;
24        }
25        if let Some(ref dt) = self.db_type {
26            return dt.eq_ignore_ascii_case(col_neutral_type);
27        }
28        false
29    }
30}
31
32/// Find the first override that matches a column and return its neutral type.
33///
34/// Returns `None` when no override matches — the caller should fall through to
35/// the default type-resolution path.
36pub fn find_override<'a>(overrides: &'a [TypeOverride], column_match: &str, col_neutral_type: &str) -> Option<&'a str> {
37    overrides.iter().find_map(|o| {
38        if o.matches(column_match, col_neutral_type) {
39            o.neutral_type.as_deref()
40        } else {
41            None
42        }
43    })
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn test_column_override_matches() {
52        let o = TypeOverride {
53            column: Some("users.metadata".to_string()),
54            db_type: None,
55            neutral_type: Some("json".to_string()),
56        };
57        assert!(o.matches("users.metadata", "jsonb"));
58        assert!(!o.matches("posts.metadata", "jsonb"));
59    }
60
61    #[test]
62    fn test_db_type_override_matches() {
63        let o = TypeOverride {
64            column: None,
65            db_type: Some("ltree".to_string()),
66            neutral_type: Some("string".to_string()),
67        };
68        assert!(o.matches("", "ltree"));
69        assert!(o.matches("any.col", "LTREE"));
70        assert!(!o.matches("any.col", "text"));
71    }
72
73    #[test]
74    fn test_column_takes_priority_over_db_type() {
75        let o = TypeOverride {
76            column: Some("users.name".to_string()),
77            db_type: Some("text".to_string()),
78            neutral_type: Some("custom".to_string()),
79        };
80        assert!(o.matches("users.name", "int32"));
81        assert!(!o.matches("other.name", "text"));
82    }
83
84    #[test]
85    fn test_find_override_first_match_wins() {
86        let overrides = vec![
87            TypeOverride {
88                column: Some("users.metadata".to_string()),
89                db_type: None,
90                neutral_type: Some("json".to_string()),
91            },
92            TypeOverride {
93                column: None,
94                db_type: Some("jsonb".to_string()),
95                neutral_type: Some("string".to_string()),
96            },
97        ];
98        assert_eq!(find_override(&overrides, "users.metadata", "jsonb"), Some("json"));
99        assert_eq!(find_override(&overrides, "posts.data", "jsonb"), Some("string"));
100        assert_eq!(find_override(&overrides, "posts.data", "text"), None);
101    }
102
103    #[test]
104    fn test_find_override_empty_list() {
105        assert_eq!(find_override(&[], "users.id", "int32"), None);
106    }
107}