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)]
144#[derive(Default)]
145pub enum FieldSelection {
146 #[default]
148 All,
149 Only(HashSet<String>),
151 Except(HashSet<String>),
153}
154
155impl FieldSelection {
156 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 pub fn is_all(&self) -> bool {
167 matches!(self, Self::All)
168 }
169}
170
171
172pub fn select(model: impl Into<String>) -> SelectSpec {
174 SelectSpec::new(model)
175}
176
177pub 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
185pub 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}