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) => 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#[derive(Debug, Clone)]
144pub enum FieldSelection {
145 All,
147 Only(HashSet<String>),
149 Except(HashSet<String>),
151}
152
153impl FieldSelection {
154 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 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
175pub fn select(model: impl Into<String>) -> SelectSpec {
177 SelectSpec::new(model)
178}
179
180pub 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
188pub 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}