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