prax_query/relations/
select.rs1use std::collections::{HashMap, HashSet};
4
5#[derive(Debug, Clone)]
7pub struct SelectSpec {
8 pub model_name: String,
10 pub fields: FieldSelection,
12 pub relations: HashMap<String, SelectSpec>,
14}
15
16impl SelectSpec {
17 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 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 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 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 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 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 pub fn relation(mut self, name: impl Into<String>, select: SelectSpec) -> Self {
85 self.relations.insert(name.into(), select);
86 self
87 }
88
89 pub fn is_field_selected(&self, field: &str) -> bool {
91 self.fields.includes(field)
92 }
93
94 pub fn selected_fields(&self) -> Option<&HashSet<String>> {
96 match &self.fields {
97 FieldSelection::Only(fields) => Some(fields),
98 _ => None,
99 }
100 }
101
102 pub fn excluded_fields(&self) -> Option<&HashSet<String>> {
104 match &self.fields {
105 FieldSelection::Except(fields) => Some(fields),
106 _ => None,
107 }
108 }
109
110 pub fn is_all(&self) -> bool {
112 matches!(self.fields, FieldSelection::All)
113 }
114
115 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#[derive(Debug, Clone)]
148pub enum FieldSelection {
149 All,
151 Only(HashSet<String>),
153 Except(HashSet<String>),
155}
156
157impl FieldSelection {
158 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 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
179pub fn select(model: impl Into<String>) -> SelectSpec {
181 SelectSpec::new(model)
182}
183
184pub 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
192pub 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