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, Default)]
144pub enum FieldSelection {
145    /// Select all fields.
146    #[default]
147    All,
148    /// Select only these fields.
149    Only(HashSet<String>),
150    /// Select all except these fields.
151    Except(HashSet<String>),
152}
153
154impl FieldSelection {
155    /// Check if a field is included in this selection.
156    pub fn includes(&self, field: &str) -> bool {
157        match self {
158            Self::All => true,
159            Self::Only(fields) => fields.contains(field),
160            Self::Except(fields) => !fields.contains(field),
161        }
162    }
163
164    /// Check if this is an "all" selection.
165    pub fn is_all(&self) -> bool {
166        matches!(self, Self::All)
167    }
168}
169
170/// Helper function to create a select spec.
171pub fn select(model: impl Into<String>) -> SelectSpec {
172    SelectSpec::new(model)
173}
174
175/// Helper function to select only specific fields.
176pub fn select_only(
177    model: impl Into<String>,
178    fields: impl IntoIterator<Item = impl Into<String>>,
179) -> SelectSpec {
180    SelectSpec::only(model, fields)
181}
182
183/// Helper function to select all fields except some.
184pub fn select_except(
185    model: impl Into<String>,
186    fields: impl IntoIterator<Item = impl Into<String>>,
187) -> SelectSpec {
188    SelectSpec::except(model, fields)
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_select_spec_all() {
197        let spec = SelectSpec::all("User");
198        assert!(spec.is_all());
199        assert!(spec.is_field_selected("id"));
200        assert!(spec.is_field_selected("email"));
201    }
202
203    #[test]
204    fn test_select_spec_only() {
205        let spec = SelectSpec::only("User", ["id", "email"]);
206        assert!(!spec.is_all());
207        assert!(spec.is_field_selected("id"));
208        assert!(spec.is_field_selected("email"));
209        assert!(!spec.is_field_selected("password"));
210    }
211
212    #[test]
213    fn test_select_spec_except() {
214        let spec = SelectSpec::except("User", ["password"]);
215        assert!(!spec.is_all());
216        assert!(spec.is_field_selected("id"));
217        assert!(!spec.is_field_selected("password"));
218    }
219
220    #[test]
221    fn test_select_spec_with_relation() {
222        let spec = SelectSpec::only("User", ["id", "name"])
223            .relation("posts", SelectSpec::only("Post", ["id", "title"]));
224
225        assert!(spec.relations.contains_key("posts"));
226    }
227
228    #[test]
229    fn test_to_sql_columns() {
230        let spec = SelectSpec::only("User", ["id", "email"]);
231        let columns = spec.to_sql_columns(&["id", "email", "name", "password"], None);
232        assert!(columns.contains("id"));
233        assert!(columns.contains("email"));
234        assert!(!columns.contains("password"));
235    }
236
237    #[test]
238    fn test_to_sql_columns_with_alias() {
239        let spec = SelectSpec::only("User", ["id", "email"]);
240        let columns = spec.to_sql_columns(&["id", "email"], Some("u"));
241        assert!(columns.contains("u.id"));
242        assert!(columns.contains("u.email"));
243    }
244}