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