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