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