sqruff_lib/core/rules/
reference.rs

1use smol_str::SmolStr;
2
3pub fn object_ref_matches_table(
4    possible_references: &[Vec<SmolStr>],
5    targets: &[Vec<SmolStr>],
6) -> bool {
7    // Simple case: If there are no references, assume okay.
8    if possible_references.is_empty() {
9        return true;
10    }
11
12    // Helper function to strip quotes from identifiers
13    let strip_quotes = |s: &str| s.trim_matches('"').to_string();
14
15    // Simple case: Reference exactly matches a target.
16    for pr in possible_references {
17        for t in targets {
18            if pr.len() == t.len()
19                && pr
20                    .iter()
21                    .zip(t.iter())
22                    .all(|(p, t)| strip_quotes(p.as_str()) == strip_quotes(t.as_str()))
23            {
24                return true;
25            }
26        }
27    }
28
29    // Handle schema-qualified table references with aliases
30    for pr in possible_references {
31        for t in targets {
32            // If the reference is just the alias (e.g. "user_profiles")
33            if pr.len() == 1
34                && t.len() == 1
35                && strip_quotes(pr[0].as_str()) == strip_quotes(t[0].as_str())
36            {
37                return true;
38            }
39            // If the reference includes schema (e.g. ["public", "user_profiles"])
40            if pr.len() == 2
41                && t.len() == 1
42                && strip_quotes(pr[1].as_str()) == strip_quotes(t[0].as_str())
43            {
44                return true;
45            }
46        }
47    }
48
49    // Tricky case: If one is shorter than the other, check for a suffix match.
50    for pr in possible_references {
51        for t in targets {
52            match pr.len().cmp(&t.len()) {
53                std::cmp::Ordering::Less => {
54                    let suffix_match = pr
55                        .iter()
56                        .zip(t[t.len() - pr.len()..].iter())
57                        .all(|(p, t)| strip_quotes(p.as_str()) == strip_quotes(t.as_str()));
58                    if suffix_match {
59                        return true;
60                    }
61                }
62                std::cmp::Ordering::Greater => {
63                    let suffix_match = t
64                        .iter()
65                        .zip(pr[pr.len() - t.len()..].iter())
66                        .all(|(t, p)| strip_quotes(t.as_str()) == strip_quotes(p.as_str()));
67                    if suffix_match {
68                        return true;
69                    }
70                }
71                std::cmp::Ordering::Equal => {}
72            }
73        }
74    }
75
76    false
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn test_object_ref_matches_table() {
85        let test_cases = vec![
86            // Empty list of references is always true
87            (vec![], vec![vec!["abc".into()]], true),
88            // Simple cases: one reference, one target
89            (
90                vec![vec!["agent1".into()]],
91                vec![vec!["agent1".into()]],
92                true,
93            ),
94            (
95                vec![vec!["agent1".into()]],
96                vec![vec!["customer".into()]],
97                false,
98            ),
99            // Multiple references. If any match, good.
100            (
101                vec![vec!["bar".into()], vec!["user_id".into()]],
102                vec![vec!["bar".into()]],
103                true,
104            ),
105            (
106                vec![vec!["foo".into()], vec!["user_id".into()]],
107                vec![vec!["bar".into()]],
108                false,
109            ),
110            // Multiple targets. If any reference matches, good.
111            (
112                vec![vec!["table1".into()]],
113                vec![
114                    vec!["table1".into()],
115                    vec!["table2".into()],
116                    vec!["table3".into()],
117                ],
118                true,
119            ),
120            (
121                vec![vec!["tbl2".into()]],
122                vec![vec!["db".into(), "sc".into(), "tbl1".into()]],
123                false,
124            ),
125            (
126                vec![vec!["tbl2".into()]],
127                vec![vec!["db".into(), "sc".into(), "tbl2".into()]],
128                true,
129            ),
130            // Multipart references and targets. Checks for a suffix match.
131            (
132                vec![vec!["Arc".into(), "tbl1".into()]],
133                vec![vec!["db".into(), "sc".into(), "tbl1".into()]],
134                false,
135            ),
136            (
137                vec![vec!["sc".into(), "tbl1".into()]],
138                vec![vec!["db".into(), "sc".into(), "tbl1".into()]],
139                true,
140            ),
141            (
142                vec![vec!["cb".into(), "sc".into(), "tbl1".into()]],
143                vec![vec!["db".into(), "sc".into(), "tbl1".into()]],
144                false,
145            ),
146            (
147                vec![vec!["db".into(), "sc".into(), "tbl1".into()]],
148                vec![vec!["db".into(), "sc".into(), "tbl1".into()]],
149                true,
150            ),
151            (
152                vec![vec!["public".into(), "agent1".into()]],
153                vec![vec!["agent1".into()]],
154                true,
155            ),
156            (
157                vec![vec!["public".into(), "agent1".into()]],
158                vec![vec!["public".into()]],
159                false,
160            ),
161        ];
162
163        for (possible_references, targets, expected) in test_cases {
164            assert_eq!(
165                object_ref_matches_table(&possible_references, &targets),
166                expected
167            );
168        }
169    }
170}