prax_query/relations/
select.rs

1//! Select specifications for choosing which fields to return.
2
3use std::collections::{HashMap, HashSet};
4
5/// Specification for which fields to select from a model.
6#[derive(Debug, Clone)]
7pub struct SelectSpec {
8    /// Model name this selection is for.
9    pub model_name: String,
10    /// Fields to include (empty means all).
11    pub fields: FieldSelection,
12    /// Relation selections.
13    pub relations: HashMap<String, SelectSpec>,
14}
15
16impl SelectSpec {
17    /// Create a new select spec for a model.
18    pub fn new(model_name: impl Into<String>) -> Self {
19        Self {
20            model_name: model_name.into(),
21            fields: FieldSelection::All,
22            relations: HashMap::new(),
23        }
24    }
25
26    /// Select all fields.
27    pub fn all(model_name: impl Into<String>) -> Self {
28        Self {
29            model_name: model_name.into(),
30            fields: FieldSelection::All,
31            relations: HashMap::new(),
32        }
33    }
34
35    /// Select only specific fields.
36    pub fn only(
37        model_name: impl Into<String>,
38        fields: impl IntoIterator<Item = impl Into<String>>,
39    ) -> Self {
40        Self {
41            model_name: model_name.into(),
42            fields: FieldSelection::Only(fields.into_iter().map(Into::into).collect()),
43            relations: HashMap::new(),
44        }
45    }
46
47    /// Exclude specific fields.
48    pub fn except(
49        model_name: impl Into<String>,
50        fields: impl IntoIterator<Item = impl Into<String>>,
51    ) -> Self {
52        Self {
53            model_name: model_name.into(),
54            fields: FieldSelection::Except(fields.into_iter().map(Into::into).collect()),
55            relations: HashMap::new(),
56        }
57    }
58
59    /// Add a field to the selection.
60    pub fn field(mut self, name: impl Into<String>) -> Self {
61        match &mut self.fields {
62            FieldSelection::All => {
63                self.fields = FieldSelection::Only(HashSet::from([name.into()]));
64            }
65            FieldSelection::Only(fields) => {
66                fields.insert(name.into());
67            }
68            FieldSelection::Except(fields) => {
69                fields.remove(&name.into());
70            }
71        }
72        self
73    }
74
75    /// Add multiple fields to the selection.
76    pub fn fields(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
77        for name in names {
78            self = self.field(name);
79        }
80        self
81    }
82
83    /// Include a relation with its selection.
84    pub fn relation(mut self, name: impl Into<String>, select: SelectSpec) -> Self {
85        self.relations.insert(name.into(), select);
86        self
87    }
88
89    /// Check if a field is selected.
90    pub fn is_field_selected(&self, field: &str) -> bool {
91        self.fields.includes(field)
92    }
93
94    /// Get the list of selected fields (if explicit).
95    pub fn selected_fields(&self) -> Option<&HashSet<String>> {
96        match &self.fields {
97            FieldSelection::Only(fields) => Some(fields),
98            _ => None,
99        }
100    }
101
102    /// Get the list of excluded fields (if explicit).
103    pub fn excluded_fields(&self) -> Option<&HashSet<String>> {
104        match &self.fields {
105            FieldSelection::Except(fields) => Some(fields),
106            _ => None,
107        }
108    }
109
110    /// Check if all fields are selected.
111    pub fn is_all(&self) -> bool {
112        matches!(self.fields, FieldSelection::All)
113    }
114
115    /// Generate the SQL column list for this selection.
116    pub fn to_sql_columns(&self, all_columns: &[&str], table_alias: Option<&str>) -> String {
117        let columns: Vec<_> = match &self.fields {
118            FieldSelection::All => all_columns.iter().map(|&s| s.to_string()).collect(),
119            FieldSelection::Only(fields) => all_columns
120                .iter()
121                .filter(|&c| fields.contains(*c))
122                .map(|&s| s.to_string())
123                .collect(),
124            FieldSelection::Except(fields) => all_columns
125                .iter()
126                .filter(|&c| !fields.contains(*c))
127                .map(|&s| s.to_string())
128                .collect(),
129        };
130
131        match table_alias {
132            Some(alias) => columns
133                .into_iter()
134                .map(|c| format!("{}.{}", alias, c))
135                .collect::<Vec<_>>()
136                .join(", "),
137            None => columns.join(", "),
138        }
139    }
140}
141
142/// Field selection mode.
143#[derive(Debug, Clone)]
144pub enum FieldSelection {
145    /// Select all fields.
146    All,
147    /// Select only these fields.
148    Only(HashSet<String>),
149    /// Select all except these fields.
150    Except(HashSet<String>),
151}
152
153impl FieldSelection {
154    /// Check if a field is included in this selection.
155    pub fn includes(&self, field: &str) -> bool {
156        match self {
157            Self::All => true,
158            Self::Only(fields) => fields.contains(field),
159            Self::Except(fields) => !fields.contains(field),
160        }
161    }
162
163    /// Check if this is an "all" selection.
164    pub fn is_all(&self) -> bool {
165        matches!(self, Self::All)
166    }
167}
168
169impl Default for FieldSelection {
170    fn default() -> Self {
171        Self::All
172    }
173}
174
175/// Helper function to create a select spec.
176pub fn select(model: impl Into<String>) -> SelectSpec {
177    SelectSpec::new(model)
178}
179
180/// Helper function to select only specific fields.
181pub fn select_only(
182    model: impl Into<String>,
183    fields: impl IntoIterator<Item = impl Into<String>>,
184) -> SelectSpec {
185    SelectSpec::only(model, fields)
186}
187
188/// Helper function to select all fields except some.
189pub fn select_except(
190    model: impl Into<String>,
191    fields: impl IntoIterator<Item = impl Into<String>>,
192) -> SelectSpec {
193    SelectSpec::except(model, fields)
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_select_spec_all() {
202        let spec = SelectSpec::all("User");
203        assert!(spec.is_all());
204        assert!(spec.is_field_selected("id"));
205        assert!(spec.is_field_selected("email"));
206    }
207
208    #[test]
209    fn test_select_spec_only() {
210        let spec = SelectSpec::only("User", ["id", "email"]);
211        assert!(!spec.is_all());
212        assert!(spec.is_field_selected("id"));
213        assert!(spec.is_field_selected("email"));
214        assert!(!spec.is_field_selected("password"));
215    }
216
217    #[test]
218    fn test_select_spec_except() {
219        let spec = SelectSpec::except("User", ["password"]);
220        assert!(!spec.is_all());
221        assert!(spec.is_field_selected("id"));
222        assert!(!spec.is_field_selected("password"));
223    }
224
225    #[test]
226    fn test_select_spec_with_relation() {
227        let spec = SelectSpec::only("User", ["id", "name"])
228            .relation("posts", SelectSpec::only("Post", ["id", "title"]));
229
230        assert!(spec.relations.contains_key("posts"));
231    }
232
233    #[test]
234    fn test_to_sql_columns() {
235        let spec = SelectSpec::only("User", ["id", "email"]);
236        let columns = spec.to_sql_columns(&["id", "email", "name", "password"], None);
237        assert!(columns.contains("id"));
238        assert!(columns.contains("email"));
239        assert!(!columns.contains("password"));
240    }
241
242    #[test]
243    fn test_to_sql_columns_with_alias() {
244        let spec = SelectSpec::only("User", ["id", "email"]);
245        let columns = spec.to_sql_columns(&["id", "email"], Some("u"));
246        assert!(columns.contains("u.id"));
247        assert!(columns.contains("u.email"));
248    }
249}