1use std::collections::{HashMap, HashSet};
4use std::path::{Path, PathBuf};
5
6use crate::cli::GenerateArgs;
7use crate::config::{CONFIG_FILE_NAME, Config, SCHEMA_FILE_PATH};
8use crate::error::CliResult;
9use crate::output::{self, success};
10use crate::schema_loader::load_schema;
11
12pub async fn run(args: GenerateArgs) -> CliResult<()> {
14 output::header("Generate Prax Client");
15
16 if args.watch {
17 output::warn("watch mode is not yet implemented; generation will run once");
18 }
19
20 let cwd = std::env::current_dir()?;
21
22 let config_path = cwd.join(CONFIG_FILE_NAME);
24 let config = if config_path.exists() {
25 Config::load(&config_path)?
26 } else {
27 Config::default()
28 };
29
30 let output_dir = args
32 .output
33 .clone()
34 .unwrap_or_else(|| PathBuf::from(&config.generator.output));
35
36 let display_path = args
37 .schema
38 .as_deref()
39 .map(|p| p.display().to_string())
40 .unwrap_or_else(|| SCHEMA_FILE_PATH.to_string());
41 output::kv("Schema", &display_path);
42 output::kv("Output", &output_dir.display().to_string());
43 output::newline();
44
45 output::step(1, 4, "Reading schema...");
46
47 let loaded = load_schema(args.schema.as_deref())?;
48 let schema = &loaded.schema;
49 if loaded.sources.len() > 1 {
50 output::kv("Source files", &loaded.sources.len().to_string());
51 }
52
53 output::step(2, 4, "Validating schema...");
54 output::step(3, 4, "Generating code...");
57
58 std::fs::create_dir_all(&output_dir)?;
60
61 let generated_files = generate_code(schema, &output_dir, &args, &config)?;
63
64 output::step(4, 4, "Writing files...");
65
66 output::newline();
68 output::section("Generated files");
69
70 for file in &generated_files {
71 let relative_path = file
72 .strip_prefix(&cwd)
73 .unwrap_or(file)
74 .display()
75 .to_string();
76 output::list_item(&relative_path);
77 }
78
79 output::newline();
80 success(&format!("Generated {} files", generated_files.len()));
81
82 Ok(())
83}
84
85fn generate_code(
87 schema: &prax_schema::ast::Schema,
88 output_dir: &Path,
89 args: &GenerateArgs,
90 config: &Config,
91) -> CliResult<Vec<PathBuf>> {
92 let mut generated_files = Vec::new();
93
94 let features = if !args.features.is_empty() {
96 args.features.clone()
97 } else {
98 config
99 .generator
100 .features
101 .clone()
102 .unwrap_or_else(|| vec!["client".to_string()])
103 };
104
105 let relation_graph = build_relation_graph(schema);
107
108 let client_path = output_dir.join("mod.rs");
110 let client_code = generate_client_module(schema, &features)?;
111 write_formatted(&client_path, &client_code)?;
112 generated_files.push(client_path);
113
114 for model in schema.models.values() {
116 let model_path = output_dir.join(format!("{}.rs", to_snake_case(model.name())));
117 let model_code = generate_model_module(model, &features, &relation_graph)?;
118 write_formatted(&model_path, &model_code)?;
119 generated_files.push(model_path);
120 }
121
122 for enum_def in schema.enums.values() {
124 let enum_path = output_dir.join(format!("{}.rs", to_snake_case(enum_def.name())));
125 let enum_code = generate_enum_module(enum_def)?;
126 write_formatted(&enum_path, &enum_code)?;
127 generated_files.push(enum_path);
128 }
129
130 let types_path = output_dir.join("types.rs");
132 let types_code = generate_types_module(schema)?;
133 write_formatted(&types_path, &types_code)?;
134 generated_files.push(types_path);
135
136 let filters_path = output_dir.join("filters.rs");
138 let filters_code = generate_filters_module(schema)?;
139 write_formatted(&filters_path, &filters_code)?;
140 generated_files.push(filters_path);
141
142 Ok(generated_files)
143}
144
145fn write_formatted(path: &Path, code: &str) -> CliResult<()> {
160 let formatted = match syn::parse_file(code) {
161 Ok(file) => prettyplease::unparse(&file),
162 Err(e) => {
163 output::warn(&format!(
164 "generated code at {} did not parse; writing unformatted. \
165 This is a codegen bug: {}",
166 path.display(),
167 e
168 ));
169 code.to_string()
170 }
171 };
172 std::fs::write(path, formatted)?;
173 Ok(())
174}
175
176fn build_relation_graph(schema: &prax_schema::ast::Schema) -> HashMap<String, HashSet<String>> {
180 let mut graph: HashMap<String, HashSet<String>> = HashMap::new();
181
182 for model in schema.models.values() {
183 let entry = graph.entry(model.name().to_string()).or_default();
184 for field in model.fields.values() {
185 if let prax_schema::ast::FieldType::Model(ref target) = field.field_type
186 && !field.is_list()
187 {
188 entry.insert(target.to_string());
189 }
190 }
191 }
192
193 graph
194}
195
196fn needs_boxing(
200 source_model: &str,
201 target_model: &str,
202 graph: &HashMap<String, HashSet<String>>,
203) -> bool {
204 let mut visited = HashSet::new();
205 let mut stack = vec![target_model.to_string()];
206
207 while let Some(current) = stack.pop() {
208 if current == source_model {
209 return true;
210 }
211 if !visited.insert(current.clone()) {
212 continue;
213 }
214 if let Some(neighbors) = graph.get(¤t) {
215 for neighbor in neighbors {
216 stack.push(neighbor.clone());
217 }
218 }
219 }
220
221 false
222}
223
224fn generate_client_module(
226 schema: &prax_schema::ast::Schema,
227 _features: &[String],
228) -> CliResult<String> {
229 let mut code = String::new();
230
231 code.push_str("//! Auto-generated by Prax - DO NOT EDIT\n");
232 code.push_str("//!\n");
233 code.push_str("//! This module contains the generated Prax client.\n\n");
234 code.push_str("#![allow(dead_code)]\n");
241 code.push_str("#![allow(clippy::derivable_impls)]\n");
242 code.push_str("#![allow(clippy::needless_update)]\n");
243 code.push_str("#![allow(clippy::too_many_arguments)]\n\n");
244
245 code.push_str("pub mod types;\n");
247 code.push_str("pub mod filters;\n\n");
248
249 for model in schema.models.values() {
250 code.push_str(&format!("pub mod {};\n", to_snake_case(model.name())));
251 }
252
253 for enum_def in schema.enums.values() {
254 code.push_str(&format!("pub mod {};\n", to_snake_case(enum_def.name())));
255 }
256
257 code.push('\n');
258
259 code.push_str("#[allow(unused_imports)]\npub use types::*;\n");
261 code.push_str("#[allow(unused_imports)]\npub use filters::*;\n\n");
262
263 for model in schema.models.values() {
264 code.push_str(&format!(
265 "#[allow(unused_imports)]\npub use {}::{};\n",
266 to_snake_case(model.name()),
267 model.name()
268 ));
269 }
270
271 for enum_def in schema.enums.values() {
272 code.push_str(&format!(
273 "#[allow(unused_imports)]\npub use {}::{};\n",
274 to_snake_case(enum_def.name()),
275 enum_def.name()
276 ));
277 }
278
279 code.push('\n');
280
281 code.push_str("#[allow(dead_code)]\n");
283 code.push_str("/// The Prax database client\n");
284 code.push_str("#[derive(Clone)]\n");
285 code.push_str("pub struct PraxClient<E: prax_query::QueryEngine> {\n");
286 code.push_str(" engine: E,\n");
287 code.push_str("}\n\n");
288
289 code.push_str("impl<E: prax_query::QueryEngine> PraxClient<E> {\n");
290 code.push_str(" /// Create a new Prax client with the given query engine\n");
291 code.push_str(" pub fn new(engine: E) -> Self {\n");
292 code.push_str(" Self { engine }\n");
293 code.push_str(" }\n\n");
294
295 code.push_str(" /// Borrow the underlying engine. Useful when composing\n");
296 code.push_str(" /// per-model operations directly or running raw SQL.\n");
297 code.push_str(" pub fn engine(&self) -> &E {\n");
298 code.push_str(" &self.engine\n");
299 code.push_str(" }\n\n");
300
301 code.push_str(" /// Execute a typed raw SQL query, decoding each returned\n");
302 code.push_str(" /// row as `T`. Mirrors `prax_orm::PraxClient::query_raw`.\n");
303 code.push_str(" ///\n");
304 code.push_str(" /// The typed per-model API covers the common CRUD cases;\n");
305 code.push_str(" /// use this for window functions, vendor-specific\n");
306 code.push_str(" /// extensions, CTEs, JOIN-driven row shapes, and aggregates\n");
307 code.push_str(" /// that the fluent builder doesn't model yet. `T` must\n");
308 code.push_str(" /// implement both `Model` (for the table association) and\n");
309 code.push_str(" /// `FromRow` (for row decoding); the generator emits both\n");
310 code.push_str(" /// impls on every model in this client.\n");
311 code.push_str(" pub async fn query_raw<T>(&self, sql: prax_query::raw::Sql)\n");
312 code.push_str(" -> prax_query::error::QueryResult<Vec<T>>\n");
313 code.push_str(" where\n");
314 code.push_str(
315 " T: prax_query::traits::Model + prax_query::row::FromRow + Send + 'static,\n",
316 );
317 code.push_str(" {\n");
318 code.push_str(" let (s, p) = sql.build();\n");
319 code.push_str(" self.engine.query_many::<T>(&s, p).await\n");
320 code.push_str(" }\n\n");
321
322 code.push_str(" /// Execute a raw statement that doesn't return rows\n");
323 code.push_str(" /// (INSERT / UPDATE / DELETE / DDL). Returns the\n");
324 code.push_str(" /// driver-reported affected-row count. Mirrors\n");
325 code.push_str(" /// `prax_orm::PraxClient::execute_raw`.\n");
326 code.push_str(" pub async fn execute_raw(&self, sql: prax_query::raw::Sql)\n");
327 code.push_str(" -> prax_query::error::QueryResult<u64>\n");
328 code.push_str(" {\n");
329 code.push_str(" let (s, p) = sql.build();\n");
330 code.push_str(" self.engine.execute_raw(&s, p).await\n");
331 code.push_str(" }\n\n");
332
333 code.push_str(" /// Run a closure inside a transaction. The closure receives\n");
334 code.push_str(" /// a fresh `PraxClient<E>` wrapping the transaction's engine;\n");
335 code.push_str(" /// every query issued through it participates in the same\n");
336 code.push_str(" /// transactional scope. Commits on `Ok`, rolls back on\n");
337 code.push_str(" /// `Err`. Mirrors `prax_orm::PraxClient::transaction`.\n");
338 code.push_str(" ///\n");
339 code.push_str(" /// Nested `transaction()` calls on the same engine currently\n");
340 code.push_str(" /// return `QueryError::internal(...)` until dialect-aware\n");
341 code.push_str(" /// SAVEPOINT support lands.\n");
342 code.push_str(" pub async fn transaction<R, Fut, F>(&self, f: F)\n");
343 code.push_str(" -> prax_query::error::QueryResult<R>\n");
344 code.push_str(" where\n");
345 code.push_str(" F: FnOnce(PraxClient<E>) -> Fut + Send + 'static,\n");
346 code.push_str(" Fut: ::core::future::Future<Output = prax_query::error::QueryResult<R>> + Send + 'static,\n");
347 code.push_str(" R: Send + 'static,\n");
348 code.push_str(" {\n");
349 code.push_str(" self.engine\n");
350 code.push_str(" .transaction(move |tx_engine| async move { f(PraxClient::new(tx_engine)).await })\n");
351 code.push_str(" .await\n");
352 code.push_str(" }\n\n");
353
354 for model in schema.models.values() {
355 let snake_name = to_snake_case(model.name());
356 code.push_str(&format!(" /// Access {} operations\n", model.name()));
357 code.push_str(&format!(
358 " pub fn {}(&self) -> {}::Client<E> {{\n",
359 snake_name, snake_name,
360 ));
361 code.push_str(&format!(
362 " {}::Client::new(self.engine.clone())\n",
363 snake_name,
364 ));
365 code.push_str(" }\n\n");
366 }
367
368 code.push_str("}\n");
369
370 Ok(code)
371}
372
373fn generate_model_module(
375 model: &prax_schema::ast::Model,
376 features: &[String],
377 relation_graph: &HashMap<String, HashSet<String>>,
378) -> CliResult<String> {
379 let mut code = String::new();
380
381 code.push_str(&format!(
382 "//! Auto-generated module for {} model\n\n",
383 model.name()
384 ));
385
386 code.push_str("#[allow(unused_imports)]\n");
388 code.push_str("use super::*;\n");
389 code.push_str("#[allow(unused_imports)]\n");
390 code.push_str("use prax_query::traits::Model;\n\n");
391
392 let mut derives = vec!["Debug", "Clone"];
394 if features.contains(&"serde".to_string()) {
395 derives.push("serde::Serialize");
396 derives.push("serde::Deserialize");
397 }
398
399 code.push_str("#[allow(dead_code)]\n");
401 code.push_str(&format!("#[derive({})]\n", derives.join(", ")));
402 code.push_str(&format!("pub struct {} {{\n", model.name()));
403
404 for field in model.fields.values() {
405 let field_name = to_field_ident(field.name());
406
407 if let Some(attr) = field.get_attribute("map")
409 && features.contains(&"serde".to_string())
410 && let Some(value) = attr.first_arg().and_then(|v| v.as_string())
411 {
412 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", value));
413 }
414
415 let rust_type = field_type_to_rust_with_boxing(
416 &field.field_type,
417 field.modifier,
418 model.name(),
419 relation_graph,
420 );
421 code.push_str(&format!(" pub {}: {},\n", field_name, rust_type));
422 }
423
424 code.push_str("}\n\n");
425
426 let table_name = model.table_name();
428 let id_fields: Vec<&str> = model.id_fields().iter().map(|f| f.name()).collect();
429 let scalar_columns: Vec<String> = model
430 .scalar_fields()
431 .iter()
432 .map(|f| {
433 f.get_attribute("map")
435 .and_then(|a| a.first_arg())
436 .and_then(|v| v.as_string())
437 .map(|s| s.to_string())
438 .unwrap_or_else(|| to_snake_case(f.name()))
439 })
440 .collect();
441
442 code.push_str(&format!("impl Model for {} {{\n", model.name()));
443 code.push_str(&format!(
444 " const MODEL_NAME: &'static str = \"{}\";\n",
445 model.name()
446 ));
447 code.push_str(&format!(
448 " const TABLE_NAME: &'static str = \"{}\";\n",
449 table_name
450 ));
451 code.push_str(&format!(
452 " const PRIMARY_KEY: &'static [&'static str] = &[{}];\n",
453 id_fields
454 .iter()
455 .map(|f| format!("\"{}\"", to_snake_case(f)))
456 .collect::<Vec<_>>()
457 .join(", ")
458 ));
459 code.push_str(&format!(
460 " const COLUMNS: &'static [&'static str] = &[{}];\n",
461 scalar_columns
462 .iter()
463 .map(|c| format!("\"{}\"", c))
464 .collect::<Vec<_>>()
465 .join(", ")
466 ));
467 code.push_str("}\n\n");
468
469 code.push_str(&format!(
475 "impl prax_query::row::FromRow for {} {{\n",
476 model.name()
477 ));
478 code.push_str(
479 " fn from_row(row: &impl prax_query::row::RowRef)\n -> Result<Self, prax_query::row::RowError>\n {\n",
480 );
481 code.push_str(" Ok(Self {\n");
482 for field in model.fields.values() {
483 let field_name = to_field_ident(field.name());
484 let rust_type = field_type_to_rust_with_boxing(
485 &field.field_type,
486 field.modifier,
487 model.name(),
488 relation_graph,
489 );
490 if field.is_relation() {
491 code.push_str(&format!(
492 " {}: ::core::default::Default::default(),\n",
493 field_name
494 ));
495 } else {
496 let column = field
497 .get_attribute("map")
498 .and_then(|a| a.first_arg())
499 .and_then(|v| v.as_string())
500 .map(|s| s.to_string())
501 .unwrap_or_else(|| field_name.clone());
502 code.push_str(&format!(
503 " {}: <{} as prax_query::row::FromColumn>::from_column(row, \"{}\")?,\n",
504 field_name, rust_type, column
505 ));
506 }
507 }
508 code.push_str(" })\n");
509 code.push_str(" }\n");
510 code.push_str("}\n\n");
511
512 code.push_str(&format!(
517 "impl prax_query::traits::ModelWithPk for {} {{\n",
518 model.name()
519 ));
520 code.push_str(" fn pk_value(&self) -> prax_query::filter::FilterValue {\n");
521 let id_field_objs: Vec<_> = model.id_fields();
522 if id_field_objs.len() == 1 {
523 let f = id_field_objs[0];
524 code.push_str(&format!(
525 " <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{})\n",
526 field_type_to_rust_with_boxing(&f.field_type, f.modifier, model.name(), relation_graph),
527 to_field_ident(f.name())
528 ));
529 } else if id_field_objs.is_empty() {
530 code.push_str(" prax_query::filter::FilterValue::Null\n");
531 } else {
532 code.push_str(" prax_query::filter::FilterValue::List(vec![\n");
533 for f in &id_field_objs {
534 code.push_str(&format!(
535 " <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{}),\n",
536 field_type_to_rust_with_boxing(&f.field_type, f.modifier, model.name(), relation_graph),
537 to_field_ident(f.name())
538 ));
539 }
540 code.push_str(" ])\n");
541 }
542 code.push_str(" }\n\n");
543
544 code.push_str(
545 " fn get_column_value(&self, column: &str)\n -> ::core::option::Option<prax_query::filter::FilterValue>\n {\n",
546 );
547 code.push_str(" match column {\n");
548 for field in model.scalar_fields() {
549 let field_name = to_field_ident(field.name());
551 let column = field
555 .get_attribute("map")
556 .and_then(|a| a.first_arg())
557 .and_then(|v| v.as_string())
558 .map(|s| s.to_string())
559 .unwrap_or_else(|| to_snake_case(field.name()));
560 let rust_type = field_type_to_rust_with_boxing(
561 &field.field_type,
562 field.modifier,
563 model.name(),
564 relation_graph,
565 );
566 code.push_str(&format!(
567 " \"{}\" => ::core::option::Option::Some(\n <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{})\n ),\n",
568 column, rust_type, field_name
569 ));
570 }
571 code.push_str(" _ => ::core::option::Option::None,\n");
572 code.push_str(" }\n");
573 code.push_str(" }\n");
574 code.push_str("}\n\n");
575
576 code.push_str(&format!(
587 "impl<E: prax_query::traits::QueryEngine>\n prax_query::traits::ModelRelationLoader<E>\n for {}\n{{\n",
588 model.name()
589 ));
590 code.push_str(" fn load_relation<'a>(\n");
591 code.push_str(" _engine: &'a E,\n");
592 code.push_str(" _parents: &'a mut [Self],\n");
593 code.push_str(" spec: &'a prax_query::relations::IncludeSpec,\n");
594 code.push_str(
595 " ) -> prax_query::traits::BoxFuture<'a, prax_query::error::QueryResult<()>> {\n",
596 );
597 code.push_str(" let name = spec.relation_name.clone();\n");
598 code.push_str(&format!(" let model_name = \"{}\";\n", model.name()));
599 code.push_str(" Box::pin(async move {\n");
600 code.push_str(" Err(prax_query::error::QueryError::internal(format!(\n");
601 code.push_str(" \"relation '{}' on schema-generated model '{}' is not wired for .include() — use query_raw for JOINs\",\n");
602 code.push_str(" name,\n");
603 code.push_str(" model_name,\n");
604 code.push_str(" )))\n");
605 code.push_str(" })\n");
606 code.push_str(" }\n");
607 code.push_str("}\n\n");
608
609 code.push_str("#[allow(dead_code)]\n");
614 code.push_str(&format!("/// Operations for the {} model\n", model.name()));
615 code.push_str("pub struct Client<E: prax_query::QueryEngine> {\n");
616 code.push_str(" engine: E,\n");
617 code.push_str("}\n\n");
618
619 code.push_str("impl<E: prax_query::QueryEngine> Client<E> {\n");
620 code.push_str(" pub fn new(engine: E) -> Self {\n");
621 code.push_str(" Self { engine }\n");
622 code.push_str(" }\n\n");
623
624 let model_ty = model.name();
625 let crud_methods: &[(&str, &str, &str)] = &[
626 ("find_many", "FindManyOperation", "Find many records"),
627 ("find_unique", "FindUniqueOperation", "Find a unique record"),
628 (
629 "find_first",
630 "FindFirstOperation",
631 "Find the first matching record",
632 ),
633 ("create", "CreateOperation", "Create a new record"),
634 (
635 "create_many",
636 "CreateManyOperation",
637 "Create many records in one operation",
638 ),
639 ("update", "UpdateOperation", "Update a record"),
640 (
641 "update_many",
642 "UpdateManyOperation",
643 "Update many records matching a filter",
644 ),
645 ("upsert", "UpsertOperation", "Insert or update a record"),
646 ("delete", "DeleteOperation", "Delete a record"),
647 (
648 "delete_many",
649 "DeleteManyOperation",
650 "Delete many records matching a filter",
651 ),
652 ("count", "CountOperation", "Count records"),
653 ];
654 for (method, op_ty, doc) in crud_methods {
655 code.push_str(&format!(" /// {}\n", doc));
656 code.push_str(&format!(
657 " pub fn {}(&self) -> prax_query::operations::{}<E, {}> {{\n",
658 method, op_ty, model_ty,
659 ));
660 code.push_str(&format!(
661 " prax_query::operations::{}::new(self.engine.clone())\n",
662 op_ty,
663 ));
664 code.push_str(" }\n\n");
665 }
666
667 code.push_str("}\n");
668
669 Ok(code)
670}
671
672fn generate_enum_module(enum_def: &prax_schema::ast::Enum) -> CliResult<String> {
674 let mut code = String::new();
675
676 code.push_str(&format!(
677 "//! Auto-generated module for {} enum\n\n",
678 enum_def.name()
679 ));
680
681 code.push_str("#[allow(dead_code)]\n");
682 code.push_str(
683 "#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\n",
684 );
685 code.push_str(&format!("pub enum {} {{\n", enum_def.name()));
686
687 for variant in &enum_def.variants {
688 let raw_name = variant.name();
689 let pascal_name = to_pascal_case(raw_name);
690
691 if let Some(attr) = variant.attributes.iter().find(|a| a.is("map"))
693 && let Some(value) = attr.first_arg().and_then(|v| v.as_string())
694 {
695 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", value));
696 code.push_str(&format!(" {},\n", pascal_name));
697 continue;
698 }
699
700 if raw_name != pascal_name {
702 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", raw_name));
703 }
704 code.push_str(&format!(" {},\n", pascal_name));
705 }
706
707 code.push_str("}\n\n");
708
709 code.push_str(&format!(
711 "impl std::fmt::Display for {} {{\n",
712 enum_def.name()
713 ));
714 code.push_str(" fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n");
715 code.push_str(" match self {\n");
716 for variant in &enum_def.variants {
717 let raw_name = variant.name();
718 let pascal_name = to_pascal_case(raw_name);
719 let db_value = variant.db_value();
720 code.push_str(&format!(
721 " Self::{} => write!(f, \"{}\"),\n",
722 pascal_name, db_value
723 ));
724 }
725 code.push_str(" }\n");
726 code.push_str(" }\n");
727 code.push_str("}\n\n");
728
729 if let Some(default_variant) = enum_def.variants.first() {
731 let pascal_name = to_pascal_case(default_variant.name());
732 code.push_str(&format!("impl Default for {} {{\n", enum_def.name()));
733 code.push_str(&format!(
734 " fn default() -> Self {{\n Self::{}\n }}\n",
735 pascal_name
736 ));
737 code.push_str("}\n\n");
738 }
739
740 code.push_str(&format!(
744 "impl std::str::FromStr for {} {{\n",
745 enum_def.name()
746 ));
747 code.push_str(" type Err = prax_query::row::RowError;\n");
748 code.push_str(" fn from_str(s: &str) -> Result<Self, Self::Err> {\n");
749 code.push_str(" match s {\n");
750 for variant in &enum_def.variants {
751 let raw_name = variant.name();
752 let pascal_name = to_pascal_case(raw_name);
753 let db_value = variant.db_value();
754 code.push_str(&format!(
755 " \"{}\" => Ok(Self::{}),\n",
756 db_value, pascal_name
757 ));
758 }
759 code.push_str(&format!(
760 " _ => Err(prax_query::row::RowError::TypeConversion {{\n column: String::new(),\n message: format!(\"unknown {} variant: {{}}\", s),\n }}),\n",
761 enum_def.name()
762 ));
763 code.push_str(" }\n");
764 code.push_str(" }\n");
765 code.push_str("}\n\n");
766
767 code.push_str(&format!(
771 "impl prax_query::row::FromColumn for {} {{\n",
772 enum_def.name()
773 ));
774 code.push_str(
775 " fn from_column(row: &impl prax_query::row::RowRef, column: &str)\n -> Result<Self, prax_query::row::RowError>\n {\n",
776 );
777 code.push_str(" let raw = row.get_string(column)?;\n");
778 code.push_str(" <Self as std::str::FromStr>::from_str(&raw).map_err(|e| {\n");
779 code.push_str(" let msg = match &e {\n");
780 code.push_str(
781 " prax_query::row::RowError::TypeConversion { message, .. } => message.clone(),\n",
782 );
783 code.push_str(" other => other.to_string(),\n");
784 code.push_str(" };\n");
785 code.push_str(" prax_query::row::RowError::TypeConversion {\n");
786 code.push_str(" column: column.to_string(),\n");
787 code.push_str(" message: msg,\n");
788 code.push_str(" }\n");
789 code.push_str(" })\n");
790 code.push_str(" }\n");
791 code.push_str("}\n\n");
792
793 code.push_str(&format!(
802 "impl prax_query::filter::ToFilterValue for {} {{\n",
803 enum_def.name()
804 ));
805 code.push_str(" fn to_filter_value(&self) -> prax_query::filter::FilterValue {\n");
806 code.push_str(" prax_query::filter::FilterValue::String(self.to_string())\n");
807 code.push_str(" }\n");
808 code.push_str("}\n");
809
810 Ok(code)
811}
812
813fn generate_types_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
815 let mut code = String::new();
816
817 code.push_str("//! Common type definitions\n\n");
818 code.push_str("#[allow(unused_imports)]\npub use chrono::{DateTime, Utc};\n");
819 code.push_str("#[allow(unused_imports)]\npub use uuid::Uuid;\n");
820 code.push_str("#[allow(unused_imports)]\npub use serde_json::Value as Json;\n");
821 code.push('\n');
822
823 for composite in schema.types.values() {
825 code.push_str("#[allow(dead_code)]\n");
826 code.push_str("#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n");
827 code.push_str(&format!("pub struct {} {{\n", composite.name()));
828 for field in composite.fields.values() {
829 let rust_type = field_type_to_rust(&field.field_type, field.modifier);
830 let field_name = to_field_ident(field.name());
831 code.push_str(&format!(" pub {}: {},\n", field_name, rust_type));
832 }
833 code.push_str("}\n\n");
834 }
835
836 Ok(code)
837}
838
839fn generate_filters_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
841 let mut code = String::new();
842
843 code.push_str("//! Filter types for queries\n\n");
844 code.push_str("#[allow(unused_imports)]\n");
845 code.push_str("use prax_query::filter::{Filter, ScalarFilter};\n");
846
847 let mut referenced_enums = HashSet::new();
849 for model in schema.models.values() {
850 for field in model.fields.values() {
851 if !field.is_relation()
852 && let prax_schema::ast::FieldType::Enum(ref name) = field.field_type
853 {
854 referenced_enums.insert(name.to_string());
855 }
856 }
857 }
858
859 for enum_name in &referenced_enums {
861 code.push_str(&format!(
862 "#[allow(unused_imports)]\nuse super::{}::{};\n",
863 to_snake_case(enum_name),
864 enum_name
865 ));
866 }
867
868 code.push('\n');
869
870 for model in schema.models.values() {
871 code.push_str("#[allow(dead_code)]\n");
873 code.push_str(&format!("/// Filter input for {} queries\n", model.name()));
874 code.push_str("#[derive(Debug, Default, Clone)]\n");
875 code.push_str(&format!("pub struct {}WhereInput {{\n", model.name()));
876
877 for field in model.fields.values() {
878 if !field.is_relation() {
879 let filter_type = field_to_filter_type(&field.field_type);
880 let field_name = to_field_ident(field.name());
881 code.push_str(&format!(
882 " pub {}: Option<{}>,\n",
883 field_name, filter_type
884 ));
885 }
886 }
887
888 code.push_str(" pub and: Option<Vec<Self>>,\n");
889 code.push_str(" pub or: Option<Vec<Self>>,\n");
890 code.push_str(" pub not: Option<Box<Self>>,\n");
891 code.push_str("}\n\n");
892
893 code.push_str("#[allow(dead_code)]\n");
895 code.push_str(&format!(
896 "/// Order by input for {} queries\n",
897 model.name()
898 ));
899 code.push_str("#[derive(Debug, Default, Clone)]\n");
900 code.push_str(&format!("pub struct {}OrderByInput {{\n", model.name()));
901
902 for field in model.fields.values() {
903 if !field.is_relation() {
904 let field_name = to_field_ident(field.name());
905 code.push_str(&format!(
906 " pub {}: Option<prax_query::SortOrder>,\n",
907 field_name
908 ));
909 }
910 }
911
912 code.push_str("}\n\n");
913 }
914
915 Ok(code)
916}
917
918fn field_type_to_rust(
920 field_type: &prax_schema::ast::FieldType,
921 modifier: prax_schema::ast::TypeModifier,
922) -> String {
923 use prax_schema::ast::{FieldType, ScalarType, TypeModifier};
924
925 let base_type = match field_type {
926 FieldType::Scalar(scalar) => match scalar {
927 ScalarType::Int => "i32".to_string(),
928 ScalarType::BigInt => "i64".to_string(),
929 ScalarType::Float => "f64".to_string(),
930 ScalarType::String => "String".to_string(),
931 ScalarType::Boolean => "bool".to_string(),
932 ScalarType::DateTime => "chrono::DateTime<chrono::Utc>".to_string(),
933 ScalarType::Date => "chrono::NaiveDate".to_string(),
934 ScalarType::Time => "chrono::NaiveTime".to_string(),
935 ScalarType::Json => "serde_json::Value".to_string(),
936 ScalarType::Bytes => "Vec<u8>".to_string(),
937 ScalarType::Decimal => "rust_decimal::Decimal".to_string(),
938 ScalarType::Uuid => "uuid::Uuid".to_string(),
939 ScalarType::Cuid => "String".to_string(),
940 ScalarType::Cuid2 => "String".to_string(),
941 ScalarType::NanoId => "String".to_string(),
942 ScalarType::Ulid => "String".to_string(),
943 ScalarType::Vector(_) | ScalarType::HalfVector(_) => "Vec<f32>".to_string(),
944 ScalarType::SparseVector(_) => "Vec<(u32, f32)>".to_string(),
945 ScalarType::Bit(_) => "Vec<u8>".to_string(),
946 },
947 FieldType::Model(name) => name.to_string(),
948 FieldType::Enum(name) => name.to_string(),
949 FieldType::Composite(name) => name.to_string(),
950 FieldType::Unsupported(_) => "serde_json::Value".to_string(),
951 };
952
953 match modifier {
954 TypeModifier::Optional | TypeModifier::OptionalList => format!("Option<{}>", base_type),
955 TypeModifier::List => format!("Vec<{}>", base_type),
956 TypeModifier::Required => base_type,
957 }
958}
959
960fn field_type_to_rust_with_boxing(
962 field_type: &prax_schema::ast::FieldType,
963 modifier: prax_schema::ast::TypeModifier,
964 source_model: &str,
965 relation_graph: &HashMap<String, HashSet<String>>,
966) -> String {
967 use prax_schema::ast::{FieldType, TypeModifier};
968
969 if let FieldType::Model(target) = field_type
978 && !matches!(modifier, TypeModifier::List)
979 {
980 let should_box = needs_boxing(source_model, target, relation_graph);
981 let base = target.to_string();
982 return if should_box {
983 format!("Option<Box<{}>>", base)
984 } else {
985 format!("Option<{}>", base)
986 };
987 }
988
989 field_type_to_rust(field_type, modifier)
991}
992
993fn field_to_filter_type(field_type: &prax_schema::ast::FieldType) -> String {
995 use prax_schema::ast::{FieldType, ScalarType};
996
997 match field_type {
998 FieldType::Scalar(scalar) => match scalar {
999 ScalarType::Int | ScalarType::BigInt => "ScalarFilter<i64>".to_string(),
1000 ScalarType::Float | ScalarType::Decimal => "ScalarFilter<f64>".to_string(),
1001 ScalarType::String
1002 | ScalarType::Uuid
1003 | ScalarType::Cuid
1004 | ScalarType::Cuid2
1005 | ScalarType::NanoId
1006 | ScalarType::Ulid => "ScalarFilter<String>".to_string(),
1007 ScalarType::Boolean => "ScalarFilter<bool>".to_string(),
1008 ScalarType::DateTime => "ScalarFilter<chrono::DateTime<chrono::Utc>>".to_string(),
1009 ScalarType::Date => "ScalarFilter<chrono::NaiveDate>".to_string(),
1010 ScalarType::Time => "ScalarFilter<chrono::NaiveTime>".to_string(),
1011 ScalarType::Json => "ScalarFilter<serde_json::Value>".to_string(),
1012 ScalarType::Bytes => "ScalarFilter<Vec<u8>>".to_string(),
1013 ScalarType::Vector(_) | ScalarType::HalfVector(_) => {
1020 "prax_pgvector::filter::VectorFilter".to_string()
1021 }
1022 ScalarType::SparseVector(_) => "ScalarFilter<Vec<(u32, f32)>>".to_string(),
1023 ScalarType::Bit(_) => "ScalarFilter<Vec<u8>>".to_string(),
1024 },
1025 FieldType::Enum(name) => format!("ScalarFilter<{}>", name),
1026 _ => "Filter".to_string(),
1027 }
1028}
1029
1030fn to_snake_case(name: &str) -> String {
1032 let mut result = String::new();
1033 for (i, c) in name.chars().enumerate() {
1034 if c.is_uppercase() {
1035 if i > 0 {
1036 result.push('_');
1037 }
1038 result.push(c.to_lowercase().next().unwrap());
1039 } else {
1040 result.push(c);
1041 }
1042 }
1043 result
1044}
1045
1046fn to_field_ident(name: &str) -> String {
1061 let snake = to_snake_case(name);
1062 if is_rust_keyword(&snake) {
1063 format!("r#{}", snake)
1064 } else {
1065 snake
1066 }
1067}
1068
1069fn is_rust_keyword(s: &str) -> bool {
1070 matches!(
1071 s,
1072 "abstract"
1073 | "as"
1074 | "async"
1075 | "await"
1076 | "become"
1077 | "box"
1078 | "break"
1079 | "const"
1080 | "continue"
1081 | "do"
1082 | "dyn"
1083 | "else"
1084 | "enum"
1085 | "extern"
1086 | "false"
1087 | "final"
1088 | "fn"
1089 | "for"
1090 | "gen"
1091 | "if"
1092 | "impl"
1093 | "in"
1094 | "let"
1095 | "loop"
1096 | "macro"
1097 | "match"
1098 | "mod"
1099 | "move"
1100 | "mut"
1101 | "override"
1102 | "priv"
1103 | "pub"
1104 | "ref"
1105 | "return"
1106 | "static"
1107 | "struct"
1108 | "trait"
1109 | "true"
1110 | "try"
1111 | "type"
1112 | "typeof"
1113 | "unsafe"
1114 | "unsized"
1115 | "use"
1116 | "virtual"
1117 | "where"
1118 | "while"
1119 | "yield"
1120 )
1121}
1122
1123fn to_pascal_case(name: &str) -> String {
1125 if name.is_empty() {
1126 return String::new();
1127 }
1128
1129 let first = name.chars().next().unwrap();
1131 if first.is_uppercase() && name.chars().any(|c| c.is_lowercase()) && !name.contains('_') {
1132 return name.to_string();
1133 }
1134
1135 name.split('_')
1137 .filter(|s| !s.is_empty())
1138 .map(|segment| {
1139 let mut chars = segment.chars();
1140 match chars.next() {
1141 None => String::new(),
1142 Some(first) => {
1143 let rest: String = chars.collect();
1144 format!("{}{}", first.to_uppercase(), rest.to_lowercase())
1145 }
1146 }
1147 })
1148 .collect()
1149}
1150
1151#[cfg(test)]
1152mod tests {
1153 use super::*;
1154
1155 #[test]
1156 fn test_to_snake_case() {
1157 assert_eq!(to_snake_case("BoardMember"), "board_member");
1158 assert_eq!(to_snake_case("User"), "user");
1159 assert_eq!(to_snake_case("JiraImportConfig"), "jira_import_config");
1160 }
1161
1162 #[test]
1163 fn test_to_pascal_case_from_snake() {
1164 assert_eq!(to_pascal_case("card_created"), "CardCreated");
1165 assert_eq!(to_pascal_case("branch_deleted"), "BranchDeleted");
1166 assert_eq!(to_pascal_case("pr_merged"), "PrMerged");
1167 }
1168
1169 #[test]
1170 fn test_to_pascal_case_from_screaming() {
1171 assert_eq!(to_pascal_case("CARD_CREATED"), "CardCreated");
1172 assert_eq!(to_pascal_case("PR_MERGED"), "PrMerged");
1173 }
1174
1175 #[test]
1176 fn test_to_pascal_case_already_pascal() {
1177 assert_eq!(to_pascal_case("Admin"), "Admin");
1178 assert_eq!(to_pascal_case("SuperAdmin"), "SuperAdmin");
1179 assert_eq!(to_pascal_case("Low"), "Low");
1180 }
1181
1182 #[test]
1183 fn test_to_pascal_case_single_word() {
1184 assert_eq!(to_pascal_case("active"), "Active");
1185 assert_eq!(to_pascal_case("ACTIVE"), "Active");
1186 }
1187
1188 #[test]
1189 fn test_needs_boxing_direct_cycle() {
1190 let mut graph = HashMap::new();
1191 graph.insert(
1192 "Board".to_string(),
1193 HashSet::from(["JiraConfig".to_string()]),
1194 );
1195 graph.insert(
1196 "JiraConfig".to_string(),
1197 HashSet::from(["Board".to_string()]),
1198 );
1199
1200 assert!(needs_boxing("Board", "JiraConfig", &graph));
1201 assert!(needs_boxing("JiraConfig", "Board", &graph));
1202 }
1203
1204 #[test]
1205 fn test_needs_boxing_no_cycle() {
1206 let mut graph = HashMap::new();
1207 graph.insert("Post".to_string(), HashSet::from(["User".to_string()]));
1208 graph.insert("User".to_string(), HashSet::new());
1209
1210 assert!(!needs_boxing("Post", "User", &graph));
1211 }
1212
1213 #[test]
1214 fn test_needs_boxing_indirect_cycle() {
1215 let mut graph = HashMap::new();
1216 graph.insert("A".to_string(), HashSet::from(["B".to_string()]));
1217 graph.insert("B".to_string(), HashSet::from(["C".to_string()]));
1218 graph.insert("C".to_string(), HashSet::from(["A".to_string()]));
1219
1220 assert!(needs_boxing("A", "B", &graph));
1221 assert!(needs_boxing("B", "C", &graph));
1222 assert!(needs_boxing("C", "A", &graph));
1223 }
1224}