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