1use scythe_backend::manifest::BackendManifest;
2use scythe_backend::naming::{NamingConfig, field_name};
3use scythe_backend::types::resolve_type_pair;
4
5use scythe_core::analyzer::{AnalyzedColumn, AnalyzedParam};
6use scythe_core::errors::{ErrorCode, ScytheError};
7
8use crate::backend_trait::{ResolvedColumn, ResolvedParam};
9use crate::overrides::{TypeOverride, find_override};
10
11pub fn resolve_columns(
17 columns: &[AnalyzedColumn],
18 manifest: &BackendManifest,
19 overrides: &[TypeOverride],
20 source_table: &str,
21) -> Result<Vec<ResolvedColumn>, ScytheError> {
22 let resolved: Vec<ResolvedColumn> = columns
23 .iter()
24 .map(|col| {
25 let column_match = if source_table.is_empty() {
26 String::new()
27 } else {
28 format!("{}.{}", source_table, col.name)
29 };
30 let effective_neutral_type =
31 find_override(overrides, &column_match, &col.neutral_type).unwrap_or(&col.neutral_type);
32
33 let (full_type, lang_type) = resolve_type_pair(effective_neutral_type, manifest, col.nullable)
34 .map(|(f, l)| (f.into_owned(), l.into_owned()))
35 .map_err(|e| {
36 ScytheError::new(
37 ErrorCode::InternalError,
38 format!("type resolution failed for column '{}': {}", col.name, e),
39 )
40 })?;
41 Ok(ResolvedColumn {
42 name: col.name.clone(),
43 field_name: field_name(&col.name, &manifest.naming).into_owned(),
44 lang_type,
45 full_type,
46 neutral_type: effective_neutral_type.to_string(),
47 nullable: col.nullable,
48 join_group: col.join_group.clone(),
49 nullable_before_join: col.nullable_before_join,
50 sql_type: col.sql_type.clone(),
51 })
52 })
53 .collect::<Result<_, ScytheError>>()?;
54
55 check_field_name_collisions(
56 resolved.iter().map(|c| (c.name.as_str(), c.field_name.as_str())),
57 "columns",
58 &manifest.naming,
59 )?;
60
61 Ok(resolved)
62}
63
64pub fn resolve_params(
69 params: &[AnalyzedParam],
70 manifest: &BackendManifest,
71 overrides: &[TypeOverride],
72 source_table: &str,
73) -> Result<Vec<ResolvedParam>, ScytheError> {
74 let resolved: Vec<ResolvedParam> = params
75 .iter()
76 .map(|param| {
77 let column_match = if source_table.is_empty() {
78 String::new()
79 } else {
80 format!("{}.{}", source_table, param.name)
81 };
82 let effective_neutral_type =
83 find_override(overrides, &column_match, ¶m.neutral_type).unwrap_or(¶m.neutral_type);
84
85 let (full_type, lang_type) = resolve_type_pair(effective_neutral_type, manifest, param.nullable)
86 .map(|(f, l)| (f.into_owned(), l.into_owned()))
87 .map_err(|e| {
88 ScytheError::new(
89 ErrorCode::InternalError,
90 format!("type resolution failed for param '{}': {}", param.name, e),
91 )
92 })?;
93 let borrowed_type = param_type_to_borrowed(&full_type);
94 Ok(ResolvedParam {
95 name: param.name.clone(),
96 field_name: field_name(¶m.name, &manifest.naming).into_owned(),
97 lang_type,
98 full_type,
99 borrowed_type,
100 neutral_type: effective_neutral_type.to_string(),
101 nullable: param.nullable,
102 })
103 })
104 .collect::<Result<_, ScytheError>>()?;
105
106 check_field_name_collisions(
107 resolved.iter().map(|p| (p.name.as_str(), p.field_name.as_str())),
108 "params",
109 &manifest.naming,
110 )?;
111
112 Ok(resolved)
113}
114
115fn check_field_name_collisions<'a>(
129 items: impl Iterator<Item = (&'a str, &'a str)>,
130 kind: &str,
131 naming: &NamingConfig,
132) -> Result<(), ScytheError> {
133 let items: Vec<(&str, &str)> = items.collect();
134 for i in 0..items.len() {
135 for j in (i + 1)..items.len() {
136 let (sql_a, field_a) = items[i];
137 let (sql_b, field_b) = items[j];
138 if field_a == field_b {
139 let alternative = if naming.field_case == "snake_case" {
140 String::new()
141 } else {
142 ", or set field_case = \"snake_case\"".to_string()
143 };
144 return Err(ScytheError::new(
145 ErrorCode::DuplicateAlias,
146 format!(
147 "{kind} '{sql_a}' and '{sql_b}' both resolve to field name '{field_a}' under \
148 field_case = \"{}\" -- alias one of them in SQL{alternative}",
149 naming.field_case
150 ),
151 ));
152 }
153 }
154 }
155 Ok(())
156}
157
158pub fn param_type_to_borrowed(rust_type: &str) -> String {
161 let copy_types = ["bool", "i16", "i32", "i64", "f32", "f64", "u64"];
162 if copy_types.contains(&rust_type) {
163 return rust_type.to_string();
164 }
165 if rust_type == "String" {
166 return "&str".to_string();
167 }
168 if let Some(inner) = rust_type.strip_prefix("Option<").and_then(|s| s.strip_suffix('>')) {
169 let borrowed_inner = param_type_to_borrowed(inner);
170 return format!("Option<{}>", borrowed_inner);
171 }
172 if let Some(inner) = rust_type.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
173 return format!("&[{}]", inner);
174 }
175 format!("&{}", rust_type)
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_param_type_to_borrowed_string() {
184 assert_eq!(param_type_to_borrowed("String"), "&str");
185 }
186
187 #[test]
188 fn test_param_type_to_borrowed_vec() {
189 assert_eq!(param_type_to_borrowed("Vec<i32>"), "&[i32]");
190 assert_eq!(param_type_to_borrowed("Vec<String>"), "&[String]");
191 }
192
193 #[test]
194 fn test_param_type_to_borrowed_passthrough() {
195 assert_eq!(param_type_to_borrowed("i32"), "i32");
196 assert_eq!(param_type_to_borrowed("i64"), "i64");
197 assert_eq!(param_type_to_borrowed("bool"), "bool");
198 assert_eq!(param_type_to_borrowed("f64"), "f64");
199 }
200
201 #[test]
202 fn test_param_type_to_borrowed_option_string() {
203 assert_eq!(param_type_to_borrowed("Option<String>"), "Option<&str>");
204 }
205
206 #[test]
207 fn test_param_type_to_borrowed_option_copy() {
208 assert_eq!(param_type_to_borrowed("Option<i32>"), "Option<i32>");
209 }
210
211 #[test]
212 fn test_param_type_to_borrowed_other() {
213 assert_eq!(param_type_to_borrowed("Uuid"), "&Uuid");
214 assert_eq!(param_type_to_borrowed("NaiveDateTime"), "&NaiveDateTime");
215 }
216
217 fn test_manifest(field_case: &str) -> BackendManifest {
218 use ahash::AHashMap;
219 use scythe_backend::manifest::{BackendMeta, TypeMappings};
220
221 let mut scalars = AHashMap::new();
222 scalars.insert("string".to_string(), "String".to_string());
223 scalars.insert("int32".to_string(), "i32".to_string());
224
225 BackendManifest {
226 backend: BackendMeta {
227 name: "test".to_string(),
228 language: "rust".to_string(),
229 file_extension: "rs".to_string(),
230 engine: "postgresql".to_string(),
231 description: None,
232 },
233 types: TypeMappings {
234 scalars,
235 containers: AHashMap::new(),
236 },
237 naming: NamingConfig {
238 struct_case: "PascalCase".to_string(),
239 fn_case: "snake_case".to_string(),
240 enum_variant_case: "PascalCase".to_string(),
241 row_suffix: "Row".to_string(),
242 field_case: field_case.to_string(),
243 },
244 imports: None,
245 }
246 }
247
248 fn test_column(name: &str) -> AnalyzedColumn {
249 AnalyzedColumn {
250 name: name.to_string(),
251 neutral_type: "string".to_string(),
252 ..Default::default()
253 }
254 }
255
256 fn test_param(name: &str, position: i64) -> AnalyzedParam {
257 AnalyzedParam {
258 name: name.to_string(),
259 neutral_type: "string".to_string(),
260 nullable: false,
261 position,
262 }
263 }
264
265 #[test]
266 fn test_resolve_columns_defaults_to_snake_case_field_names() {
267 let manifest = test_manifest("snake_case");
268 let resolved = resolve_columns(&[test_column("UserId")], &manifest, &[], "").unwrap();
269 assert_eq!(resolved[0].field_name, "user_id");
270 }
271
272 #[test]
273 fn test_resolve_columns_honors_camel_case_field_case() {
274 let manifest = test_manifest("camelCase");
275 let resolved = resolve_columns(&[test_column("user_id")], &manifest, &[], "").unwrap();
276 assert_eq!(resolved[0].field_name, "userId");
277 }
278
279 #[test]
280 fn test_resolve_params_honors_camel_case_field_case() {
281 let manifest = test_manifest("camelCase");
282 let resolved = resolve_params(&[test_param("user_id", 1)], &manifest, &[], "").unwrap();
283 assert_eq!(resolved[0].field_name, "userId");
284 }
285
286 #[test]
296 fn test_resolve_columns_rejects_collision_under_snake_case() {
297 let manifest = test_manifest("snake_case");
298 let err = resolve_columns(&[test_column("user_id"), test_column("USER_ID")], &manifest, &[], "")
299 .expect_err("user_id and USER_ID must collide under snake_case too");
300 let message = err.to_string();
301 assert!(message.contains("user_id"), "{message}");
302 assert!(message.contains("USER_ID"), "{message}");
303 assert!(
304 !message.contains("or set field_case"),
305 "already snake_case -- switching field_case cannot fix this: {message}"
306 );
307 }
308
309 #[test]
310 fn test_resolve_columns_rejects_collision_under_camel_case() {
311 let manifest = test_manifest("camelCase");
312 let err = resolve_columns(&[test_column("user_id"), test_column("userId")], &manifest, &[], "")
313 .expect_err("user_id and userId must collide under camelCase");
314 let message = err.to_string();
315 assert!(message.contains("user_id"), "{message}");
316 assert!(message.contains("userId"), "{message}");
317 assert!(message.contains("field_case"), "{message}");
318 }
319
320 #[test]
321 fn test_resolve_columns_rejects_collision_across_four_spellings() {
322 let manifest = test_manifest("camelCase");
323 for (a, b) in [
324 ("user_id", "USER_ID"),
325 ("user_id", "UserId"),
326 ("USER_ID", "userId"),
327 ("UserId", "userId"),
328 ] {
329 resolve_columns(&[test_column(a), test_column(b)], &manifest, &[], "")
330 .expect_err(&format!("{a} and {b} must collide under camelCase"));
331 }
332 }
333
334 #[test]
335 fn test_resolve_columns_rejects_double_underscore_collision() {
336 let manifest = test_manifest("camelCase");
337 resolve_columns(&[test_column("a_b"), test_column("a__b")], &manifest, &[], "")
338 .expect_err("a_b and a__b must both collapse to aB under camelCase");
339 }
340
341 #[test]
342 fn test_resolve_columns_rejects_leading_underscore_collision() {
343 let manifest = test_manifest("camelCase");
344 resolve_columns(&[test_column("id"), test_column("_id")], &manifest, &[], "")
345 .expect_err("id and _id must both collapse to id under camelCase");
346 }
347
348 #[test]
349 fn test_resolve_params_rejects_collision_under_camel_case() {
350 let manifest = test_manifest("camelCase");
351 let err = resolve_params(&[test_param("user_id", 1), test_param("userId", 2)], &manifest, &[], "")
352 .expect_err("user_id and userId must collide under camelCase");
353 assert!(err.to_string().contains("params"), "{err}");
354 }
355
356 #[test]
357 fn test_resolve_columns_no_collision_when_names_differ() {
358 let manifest = test_manifest("camelCase");
359 let resolved = resolve_columns(&[test_column("user_id"), test_column("order_id")], &manifest, &[], "").unwrap();
360 assert_eq!(resolved[0].field_name, "userId");
361 assert_eq!(resolved[1].field_name, "orderId");
362 }
363}