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
224 code.push_str("pub mod types;\n");
226 code.push_str("pub mod filters;\n\n");
227
228 for model in schema.models.values() {
229 code.push_str(&format!("pub mod {};\n", to_snake_case(model.name())));
230 }
231
232 for enum_def in schema.enums.values() {
233 code.push_str(&format!("pub mod {};\n", to_snake_case(enum_def.name())));
234 }
235
236 code.push('\n');
237
238 code.push_str("#[allow(unused_imports)]\npub use types::*;\n");
240 code.push_str("#[allow(unused_imports)]\npub use filters::*;\n\n");
241
242 for model in schema.models.values() {
243 code.push_str(&format!(
244 "#[allow(unused_imports)]\npub use {}::{};\n",
245 to_snake_case(model.name()),
246 model.name()
247 ));
248 }
249
250 for enum_def in schema.enums.values() {
251 code.push_str(&format!(
252 "#[allow(unused_imports)]\npub use {}::{};\n",
253 to_snake_case(enum_def.name()),
254 enum_def.name()
255 ));
256 }
257
258 code.push('\n');
259
260 code.push_str("#[allow(dead_code)]\n");
262 code.push_str("/// The Prax database client\n");
263 code.push_str("#[derive(Clone)]\n");
264 code.push_str("pub struct PraxClient<E: prax_query::QueryEngine> {\n");
265 code.push_str(" engine: E,\n");
266 code.push_str("}\n\n");
267
268 code.push_str("impl<E: prax_query::QueryEngine> PraxClient<E> {\n");
269 code.push_str(" /// Create a new Prax client with the given query engine\n");
270 code.push_str(" pub fn new(engine: E) -> Self {\n");
271 code.push_str(" Self { engine }\n");
272 code.push_str(" }\n\n");
273
274 code.push_str(" /// Borrow the underlying engine. Useful when composing\n");
275 code.push_str(" /// per-model operations directly or running raw SQL.\n");
276 code.push_str(" pub fn engine(&self) -> &E {\n");
277 code.push_str(" &self.engine\n");
278 code.push_str(" }\n\n");
279
280 code.push_str(" /// Execute a typed raw SQL query, decoding each returned\n");
281 code.push_str(" /// row as `T`. Mirrors `prax_orm::PraxClient::query_raw`.\n");
282 code.push_str(" ///\n");
283 code.push_str(" /// The typed per-model API covers the common CRUD cases;\n");
284 code.push_str(" /// use this for window functions, vendor-specific\n");
285 code.push_str(" /// extensions, CTEs, JOIN-driven row shapes, and aggregates\n");
286 code.push_str(" /// that the fluent builder doesn't model yet. `T` must\n");
287 code.push_str(" /// implement both `Model` (for the table association) and\n");
288 code.push_str(" /// `FromRow` (for row decoding); the generator emits both\n");
289 code.push_str(" /// impls on every model in this client.\n");
290 code.push_str(" pub async fn query_raw<T>(&self, sql: prax_query::raw::Sql)\n");
291 code.push_str(" -> prax_query::error::QueryResult<Vec<T>>\n");
292 code.push_str(" where\n");
293 code.push_str(
294 " T: prax_query::traits::Model + prax_query::row::FromRow + Send + 'static,\n",
295 );
296 code.push_str(" {\n");
297 code.push_str(" let (s, p) = sql.build();\n");
298 code.push_str(" self.engine.query_many::<T>(&s, p).await\n");
299 code.push_str(" }\n\n");
300
301 code.push_str(" /// Execute a raw statement that doesn't return rows\n");
302 code.push_str(" /// (INSERT / UPDATE / DELETE / DDL). Returns the\n");
303 code.push_str(" /// driver-reported affected-row count. Mirrors\n");
304 code.push_str(" /// `prax_orm::PraxClient::execute_raw`.\n");
305 code.push_str(" pub async fn execute_raw(&self, sql: prax_query::raw::Sql)\n");
306 code.push_str(" -> prax_query::error::QueryResult<u64>\n");
307 code.push_str(" {\n");
308 code.push_str(" let (s, p) = sql.build();\n");
309 code.push_str(" self.engine.execute_raw(&s, p).await\n");
310 code.push_str(" }\n\n");
311
312 code.push_str(" /// Run a closure inside a transaction. The closure receives\n");
313 code.push_str(" /// a fresh `PraxClient<E>` wrapping the transaction's engine;\n");
314 code.push_str(" /// every query issued through it participates in the same\n");
315 code.push_str(" /// transactional scope. Commits on `Ok`, rolls back on\n");
316 code.push_str(" /// `Err`. Mirrors `prax_orm::PraxClient::transaction`.\n");
317 code.push_str(" ///\n");
318 code.push_str(" /// Nested `transaction()` calls on the same engine currently\n");
319 code.push_str(" /// return `QueryError::internal(...)` until dialect-aware\n");
320 code.push_str(" /// SAVEPOINT support lands.\n");
321 code.push_str(" pub async fn transaction<R, Fut, F>(&self, f: F)\n");
322 code.push_str(" -> prax_query::error::QueryResult<R>\n");
323 code.push_str(" where\n");
324 code.push_str(" F: FnOnce(PraxClient<E>) -> Fut + Send + 'static,\n");
325 code.push_str(" Fut: ::core::future::Future<Output = prax_query::error::QueryResult<R>> + Send + 'static,\n");
326 code.push_str(" R: Send + 'static,\n");
327 code.push_str(" {\n");
328 code.push_str(" self.engine\n");
329 code.push_str(" .transaction(move |tx_engine| async move { f(PraxClient::new(tx_engine)).await })\n");
330 code.push_str(" .await\n");
331 code.push_str(" }\n\n");
332
333 for model in schema.models.values() {
334 let snake_name = to_snake_case(model.name());
335 code.push_str(&format!(" /// Access {} operations\n", model.name()));
336 code.push_str(&format!(
337 " pub fn {}(&self) -> {}::Client<E> {{\n",
338 snake_name, snake_name,
339 ));
340 code.push_str(&format!(
341 " {}::Client::new(self.engine.clone())\n",
342 snake_name,
343 ));
344 code.push_str(" }\n\n");
345 }
346
347 code.push_str("}\n");
348
349 Ok(code)
350}
351
352fn generate_model_module(
354 model: &prax_schema::ast::Model,
355 features: &[String],
356 relation_graph: &HashMap<String, HashSet<String>>,
357) -> CliResult<String> {
358 let mut code = String::new();
359
360 code.push_str(&format!(
361 "//! Auto-generated module for {} model\n\n",
362 model.name()
363 ));
364
365 code.push_str("#[allow(unused_imports)]\n");
367 code.push_str("use super::*;\n");
368 code.push_str("#[allow(unused_imports)]\n");
369 code.push_str("use prax_query::traits::Model;\n\n");
370
371 let mut derives = vec!["Debug", "Clone"];
373 if features.contains(&"serde".to_string()) {
374 derives.push("serde::Serialize");
375 derives.push("serde::Deserialize");
376 }
377
378 code.push_str("#[allow(dead_code)]\n");
380 code.push_str(&format!("#[derive({})]\n", derives.join(", ")));
381 code.push_str(&format!("pub struct {} {{\n", model.name()));
382
383 for field in model.fields.values() {
384 let field_name = to_field_ident(field.name());
385
386 if let Some(attr) = field.get_attribute("map")
388 && features.contains(&"serde".to_string())
389 && let Some(value) = attr.first_arg().and_then(|v| v.as_string())
390 {
391 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", value));
392 }
393
394 let rust_type = field_type_to_rust_with_boxing(
395 &field.field_type,
396 field.modifier,
397 model.name(),
398 relation_graph,
399 );
400 code.push_str(&format!(" pub {}: {},\n", field_name, rust_type));
401 }
402
403 code.push_str("}\n\n");
404
405 let table_name = model.table_name();
407 let id_fields: Vec<&str> = model.id_fields().iter().map(|f| f.name()).collect();
408 let scalar_columns: Vec<String> = model
409 .scalar_fields()
410 .iter()
411 .map(|f| {
412 f.get_attribute("map")
414 .and_then(|a| a.first_arg())
415 .and_then(|v| v.as_string())
416 .map(|s| s.to_string())
417 .unwrap_or_else(|| to_snake_case(f.name()))
418 })
419 .collect();
420
421 code.push_str(&format!("impl Model for {} {{\n", model.name()));
422 code.push_str(&format!(
423 " const MODEL_NAME: &'static str = \"{}\";\n",
424 model.name()
425 ));
426 code.push_str(&format!(
427 " const TABLE_NAME: &'static str = \"{}\";\n",
428 table_name
429 ));
430 code.push_str(&format!(
431 " const PRIMARY_KEY: &'static [&'static str] = &[{}];\n",
432 id_fields
433 .iter()
434 .map(|f| format!("\"{}\"", to_snake_case(f)))
435 .collect::<Vec<_>>()
436 .join(", ")
437 ));
438 code.push_str(&format!(
439 " const COLUMNS: &'static [&'static str] = &[{}];\n",
440 scalar_columns
441 .iter()
442 .map(|c| format!("\"{}\"", c))
443 .collect::<Vec<_>>()
444 .join(", ")
445 ));
446 code.push_str("}\n\n");
447
448 code.push_str(&format!(
454 "impl prax_query::row::FromRow for {} {{\n",
455 model.name()
456 ));
457 code.push_str(
458 " fn from_row(row: &impl prax_query::row::RowRef)\n -> Result<Self, prax_query::row::RowError>\n {\n",
459 );
460 code.push_str(" Ok(Self {\n");
461 for field in model.fields.values() {
462 let field_name = to_field_ident(field.name());
463 let rust_type = field_type_to_rust_with_boxing(
464 &field.field_type,
465 field.modifier,
466 model.name(),
467 relation_graph,
468 );
469 if field.is_relation() {
470 code.push_str(&format!(
471 " {}: ::core::default::Default::default(),\n",
472 field_name
473 ));
474 } else {
475 let column = field
476 .get_attribute("map")
477 .and_then(|a| a.first_arg())
478 .and_then(|v| v.as_string())
479 .map(|s| s.to_string())
480 .unwrap_or_else(|| field_name.clone());
481 code.push_str(&format!(
482 " {}: <{} as prax_query::row::FromColumn>::from_column(row, \"{}\")?,\n",
483 field_name, rust_type, column
484 ));
485 }
486 }
487 code.push_str(" })\n");
488 code.push_str(" }\n");
489 code.push_str("}\n\n");
490
491 code.push_str(&format!(
496 "impl prax_query::traits::ModelWithPk for {} {{\n",
497 model.name()
498 ));
499 code.push_str(" fn pk_value(&self) -> prax_query::filter::FilterValue {\n");
500 let id_field_objs: Vec<_> = model.id_fields();
501 if id_field_objs.len() == 1 {
502 let f = id_field_objs[0];
503 code.push_str(&format!(
504 " <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{})\n",
505 field_type_to_rust_with_boxing(&f.field_type, f.modifier, model.name(), relation_graph),
506 to_field_ident(f.name())
507 ));
508 } else if id_field_objs.is_empty() {
509 code.push_str(" prax_query::filter::FilterValue::Null\n");
510 } else {
511 code.push_str(" prax_query::filter::FilterValue::List(vec![\n");
512 for f in &id_field_objs {
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 }
519 code.push_str(" ])\n");
520 }
521 code.push_str(" }\n\n");
522
523 code.push_str(
524 " fn get_column_value(&self, column: &str)\n -> ::core::option::Option<prax_query::filter::FilterValue>\n {\n",
525 );
526 code.push_str(" match column {\n");
527 for field in model.scalar_fields() {
528 let field_name = to_field_ident(field.name());
530 let column = field
534 .get_attribute("map")
535 .and_then(|a| a.first_arg())
536 .and_then(|v| v.as_string())
537 .map(|s| s.to_string())
538 .unwrap_or_else(|| to_snake_case(field.name()));
539 let rust_type = field_type_to_rust_with_boxing(
540 &field.field_type,
541 field.modifier,
542 model.name(),
543 relation_graph,
544 );
545 code.push_str(&format!(
546 " \"{}\" => ::core::option::Option::Some(\n <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{})\n ),\n",
547 column, rust_type, field_name
548 ));
549 }
550 code.push_str(" _ => ::core::option::Option::None,\n");
551 code.push_str(" }\n");
552 code.push_str(" }\n");
553 code.push_str("}\n\n");
554
555 code.push_str(&format!(
566 "impl<E: prax_query::traits::QueryEngine>\n prax_query::traits::ModelRelationLoader<E>\n for {}\n{{\n",
567 model.name()
568 ));
569 code.push_str(" fn load_relation<'a>(\n");
570 code.push_str(" _engine: &'a E,\n");
571 code.push_str(" _parents: &'a mut [Self],\n");
572 code.push_str(" spec: &'a prax_query::relations::IncludeSpec,\n");
573 code.push_str(
574 " ) -> prax_query::traits::BoxFuture<'a, prax_query::error::QueryResult<()>> {\n",
575 );
576 code.push_str(" let name = spec.relation_name.clone();\n");
577 code.push_str(&format!(" let model_name = \"{}\";\n", model.name()));
578 code.push_str(" Box::pin(async move {\n");
579 code.push_str(" Err(prax_query::error::QueryError::internal(format!(\n");
580 code.push_str(" \"relation '{}' on schema-generated model '{}' is not wired for .include() — use query_raw for JOINs\",\n");
581 code.push_str(" name,\n");
582 code.push_str(" model_name,\n");
583 code.push_str(" )))\n");
584 code.push_str(" })\n");
585 code.push_str(" }\n");
586 code.push_str("}\n\n");
587
588 code.push_str("#[allow(dead_code)]\n");
593 code.push_str(&format!("/// Operations for the {} model\n", model.name()));
594 code.push_str("pub struct Client<E: prax_query::QueryEngine> {\n");
595 code.push_str(" engine: E,\n");
596 code.push_str("}\n\n");
597
598 code.push_str("impl<E: prax_query::QueryEngine> Client<E> {\n");
599 code.push_str(" pub fn new(engine: E) -> Self {\n");
600 code.push_str(" Self { engine }\n");
601 code.push_str(" }\n\n");
602
603 let model_ty = model.name();
604 let crud_methods: &[(&str, &str, &str)] = &[
605 ("find_many", "FindManyOperation", "Find many records"),
606 ("find_unique", "FindUniqueOperation", "Find a unique record"),
607 (
608 "find_first",
609 "FindFirstOperation",
610 "Find the first matching record",
611 ),
612 ("create", "CreateOperation", "Create a new record"),
613 (
614 "create_many",
615 "CreateManyOperation",
616 "Create many records in one operation",
617 ),
618 ("update", "UpdateOperation", "Update a record"),
619 (
620 "update_many",
621 "UpdateManyOperation",
622 "Update many records matching a filter",
623 ),
624 ("upsert", "UpsertOperation", "Insert or update a record"),
625 ("delete", "DeleteOperation", "Delete a record"),
626 (
627 "delete_many",
628 "DeleteManyOperation",
629 "Delete many records matching a filter",
630 ),
631 ("count", "CountOperation", "Count records"),
632 ];
633 for (method, op_ty, doc) in crud_methods {
634 code.push_str(&format!(" /// {}\n", doc));
635 code.push_str(&format!(
636 " pub fn {}(&self) -> prax_query::operations::{}<E, {}> {{\n",
637 method, op_ty, model_ty,
638 ));
639 code.push_str(&format!(
640 " prax_query::operations::{}::new(self.engine.clone())\n",
641 op_ty,
642 ));
643 code.push_str(" }\n\n");
644 }
645
646 code.push_str("}\n");
647
648 Ok(code)
649}
650
651fn generate_enum_module(enum_def: &prax_schema::ast::Enum) -> CliResult<String> {
653 let mut code = String::new();
654
655 code.push_str(&format!(
656 "//! Auto-generated module for {} enum\n\n",
657 enum_def.name()
658 ));
659
660 code.push_str("#[allow(dead_code)]\n");
661 code.push_str(
662 "#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\n",
663 );
664 code.push_str(&format!("pub enum {} {{\n", enum_def.name()));
665
666 for variant in &enum_def.variants {
667 let raw_name = variant.name();
668 let pascal_name = to_pascal_case(raw_name);
669
670 if let Some(attr) = variant.attributes.iter().find(|a| a.is("map"))
672 && let Some(value) = attr.first_arg().and_then(|v| v.as_string())
673 {
674 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", value));
675 code.push_str(&format!(" {},\n", pascal_name));
676 continue;
677 }
678
679 if raw_name != pascal_name {
681 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", raw_name));
682 }
683 code.push_str(&format!(" {},\n", pascal_name));
684 }
685
686 code.push_str("}\n\n");
687
688 code.push_str(&format!(
690 "impl std::fmt::Display for {} {{\n",
691 enum_def.name()
692 ));
693 code.push_str(" fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n");
694 code.push_str(" match self {\n");
695 for variant in &enum_def.variants {
696 let raw_name = variant.name();
697 let pascal_name = to_pascal_case(raw_name);
698 let db_value = variant.db_value();
699 code.push_str(&format!(
700 " Self::{} => write!(f, \"{}\"),\n",
701 pascal_name, db_value
702 ));
703 }
704 code.push_str(" }\n");
705 code.push_str(" }\n");
706 code.push_str("}\n\n");
707
708 if let Some(default_variant) = enum_def.variants.first() {
710 let pascal_name = to_pascal_case(default_variant.name());
711 code.push_str(&format!("impl Default for {} {{\n", enum_def.name()));
712 code.push_str(&format!(
713 " fn default() -> Self {{\n Self::{}\n }}\n",
714 pascal_name
715 ));
716 code.push_str("}\n\n");
717 }
718
719 code.push_str(&format!(
723 "impl std::str::FromStr for {} {{\n",
724 enum_def.name()
725 ));
726 code.push_str(" type Err = prax_query::row::RowError;\n");
727 code.push_str(" fn from_str(s: &str) -> Result<Self, Self::Err> {\n");
728 code.push_str(" match s {\n");
729 for variant in &enum_def.variants {
730 let raw_name = variant.name();
731 let pascal_name = to_pascal_case(raw_name);
732 let db_value = variant.db_value();
733 code.push_str(&format!(
734 " \"{}\" => Ok(Self::{}),\n",
735 db_value, pascal_name
736 ));
737 }
738 code.push_str(&format!(
739 " _ => Err(prax_query::row::RowError::TypeConversion {{\n column: String::new(),\n message: format!(\"unknown {} variant: {{}}\", s),\n }}),\n",
740 enum_def.name()
741 ));
742 code.push_str(" }\n");
743 code.push_str(" }\n");
744 code.push_str("}\n\n");
745
746 code.push_str(&format!(
750 "impl prax_query::row::FromColumn for {} {{\n",
751 enum_def.name()
752 ));
753 code.push_str(
754 " fn from_column(row: &impl prax_query::row::RowRef, column: &str)\n -> Result<Self, prax_query::row::RowError>\n {\n",
755 );
756 code.push_str(" let raw = row.get_string(column)?;\n");
757 code.push_str(" <Self as std::str::FromStr>::from_str(&raw).map_err(|e| {\n");
758 code.push_str(" let msg = match &e {\n");
759 code.push_str(
760 " prax_query::row::RowError::TypeConversion { message, .. } => message.clone(),\n",
761 );
762 code.push_str(" other => other.to_string(),\n");
763 code.push_str(" };\n");
764 code.push_str(" prax_query::row::RowError::TypeConversion {\n");
765 code.push_str(" column: column.to_string(),\n");
766 code.push_str(" message: msg,\n");
767 code.push_str(" }\n");
768 code.push_str(" })\n");
769 code.push_str(" }\n");
770 code.push_str("}\n\n");
771
772 code.push_str(&format!(
781 "impl prax_query::filter::ToFilterValue for {} {{\n",
782 enum_def.name()
783 ));
784 code.push_str(" fn to_filter_value(&self) -> prax_query::filter::FilterValue {\n");
785 code.push_str(" prax_query::filter::FilterValue::String(self.to_string())\n");
786 code.push_str(" }\n");
787 code.push_str("}\n");
788
789 Ok(code)
790}
791
792fn generate_types_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
794 let mut code = String::new();
795
796 code.push_str("//! Common type definitions\n\n");
797 code.push_str("#[allow(unused_imports)]\npub use chrono::{DateTime, Utc};\n");
798 code.push_str("#[allow(unused_imports)]\npub use uuid::Uuid;\n");
799 code.push_str("#[allow(unused_imports)]\npub use serde_json::Value as Json;\n");
800 code.push('\n');
801
802 for composite in schema.types.values() {
804 code.push_str("#[allow(dead_code)]\n");
805 code.push_str("#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n");
806 code.push_str(&format!("pub struct {} {{\n", composite.name()));
807 for field in composite.fields.values() {
808 let rust_type = field_type_to_rust(&field.field_type, field.modifier);
809 let field_name = to_field_ident(field.name());
810 code.push_str(&format!(" pub {}: {},\n", field_name, rust_type));
811 }
812 code.push_str("}\n\n");
813 }
814
815 Ok(code)
816}
817
818fn generate_filters_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
820 let mut code = String::new();
821
822 code.push_str("//! Filter types for queries\n\n");
823 code.push_str("#[allow(unused_imports)]\n");
824 code.push_str("use prax_query::filter::{Filter, ScalarFilter};\n");
825
826 let mut referenced_enums = HashSet::new();
828 for model in schema.models.values() {
829 for field in model.fields.values() {
830 if !field.is_relation()
831 && let prax_schema::ast::FieldType::Enum(ref name) = field.field_type
832 {
833 referenced_enums.insert(name.to_string());
834 }
835 }
836 }
837
838 for enum_name in &referenced_enums {
840 code.push_str(&format!(
841 "#[allow(unused_imports)]\nuse super::{}::{};\n",
842 to_snake_case(enum_name),
843 enum_name
844 ));
845 }
846
847 code.push('\n');
848
849 for model in schema.models.values() {
850 code.push_str("#[allow(dead_code)]\n");
852 code.push_str(&format!("/// Filter input for {} queries\n", model.name()));
853 code.push_str("#[derive(Debug, Default, Clone)]\n");
854 code.push_str(&format!("pub struct {}WhereInput {{\n", model.name()));
855
856 for field in model.fields.values() {
857 if !field.is_relation() {
858 let filter_type = field_to_filter_type(&field.field_type);
859 let field_name = to_field_ident(field.name());
860 code.push_str(&format!(
861 " pub {}: Option<{}>,\n",
862 field_name, filter_type
863 ));
864 }
865 }
866
867 code.push_str(" pub and: Option<Vec<Self>>,\n");
868 code.push_str(" pub or: Option<Vec<Self>>,\n");
869 code.push_str(" pub not: Option<Box<Self>>,\n");
870 code.push_str("}\n\n");
871
872 code.push_str("#[allow(dead_code)]\n");
874 code.push_str(&format!(
875 "/// Order by input for {} queries\n",
876 model.name()
877 ));
878 code.push_str("#[derive(Debug, Default, Clone)]\n");
879 code.push_str(&format!("pub struct {}OrderByInput {{\n", model.name()));
880
881 for field in model.fields.values() {
882 if !field.is_relation() {
883 let field_name = to_field_ident(field.name());
884 code.push_str(&format!(
885 " pub {}: Option<prax_query::SortOrder>,\n",
886 field_name
887 ));
888 }
889 }
890
891 code.push_str("}\n\n");
892 }
893
894 Ok(code)
895}
896
897fn field_type_to_rust(
899 field_type: &prax_schema::ast::FieldType,
900 modifier: prax_schema::ast::TypeModifier,
901) -> String {
902 use prax_schema::ast::{FieldType, ScalarType, TypeModifier};
903
904 let base_type = match field_type {
905 FieldType::Scalar(scalar) => match scalar {
906 ScalarType::Int => "i32".to_string(),
907 ScalarType::BigInt => "i64".to_string(),
908 ScalarType::Float => "f64".to_string(),
909 ScalarType::String => "String".to_string(),
910 ScalarType::Boolean => "bool".to_string(),
911 ScalarType::DateTime => "chrono::DateTime<chrono::Utc>".to_string(),
912 ScalarType::Date => "chrono::NaiveDate".to_string(),
913 ScalarType::Time => "chrono::NaiveTime".to_string(),
914 ScalarType::Json => "serde_json::Value".to_string(),
915 ScalarType::Bytes => "Vec<u8>".to_string(),
916 ScalarType::Decimal => "rust_decimal::Decimal".to_string(),
917 ScalarType::Uuid => "uuid::Uuid".to_string(),
918 ScalarType::Cuid => "String".to_string(),
919 ScalarType::Cuid2 => "String".to_string(),
920 ScalarType::NanoId => "String".to_string(),
921 ScalarType::Ulid => "String".to_string(),
922 ScalarType::Vector(_) | ScalarType::HalfVector(_) => "Vec<f32>".to_string(),
923 ScalarType::SparseVector(_) => "Vec<(u32, f32)>".to_string(),
924 ScalarType::Bit(_) => "Vec<u8>".to_string(),
925 },
926 FieldType::Model(name) => name.to_string(),
927 FieldType::Enum(name) => name.to_string(),
928 FieldType::Composite(name) => name.to_string(),
929 FieldType::Unsupported(_) => "serde_json::Value".to_string(),
930 };
931
932 match modifier {
933 TypeModifier::Optional | TypeModifier::OptionalList => format!("Option<{}>", base_type),
934 TypeModifier::List => format!("Vec<{}>", base_type),
935 TypeModifier::Required => base_type,
936 }
937}
938
939fn field_type_to_rust_with_boxing(
941 field_type: &prax_schema::ast::FieldType,
942 modifier: prax_schema::ast::TypeModifier,
943 source_model: &str,
944 relation_graph: &HashMap<String, HashSet<String>>,
945) -> String {
946 use prax_schema::ast::{FieldType, TypeModifier};
947
948 if let FieldType::Model(target) = field_type
957 && !matches!(modifier, TypeModifier::List)
958 {
959 let should_box = needs_boxing(source_model, target, relation_graph);
960 let base = target.to_string();
961 return if should_box {
962 format!("Option<Box<{}>>", base)
963 } else {
964 format!("Option<{}>", base)
965 };
966 }
967
968 field_type_to_rust(field_type, modifier)
970}
971
972fn field_to_filter_type(field_type: &prax_schema::ast::FieldType) -> String {
974 use prax_schema::ast::{FieldType, ScalarType};
975
976 match field_type {
977 FieldType::Scalar(scalar) => match scalar {
978 ScalarType::Int | ScalarType::BigInt => "ScalarFilter<i64>".to_string(),
979 ScalarType::Float | ScalarType::Decimal => "ScalarFilter<f64>".to_string(),
980 ScalarType::String
981 | ScalarType::Uuid
982 | ScalarType::Cuid
983 | ScalarType::Cuid2
984 | ScalarType::NanoId
985 | ScalarType::Ulid => "ScalarFilter<String>".to_string(),
986 ScalarType::Boolean => "ScalarFilter<bool>".to_string(),
987 ScalarType::DateTime => "ScalarFilter<chrono::DateTime<chrono::Utc>>".to_string(),
988 ScalarType::Date => "ScalarFilter<chrono::NaiveDate>".to_string(),
989 ScalarType::Time => "ScalarFilter<chrono::NaiveTime>".to_string(),
990 ScalarType::Json => "ScalarFilter<serde_json::Value>".to_string(),
991 ScalarType::Bytes => "ScalarFilter<Vec<u8>>".to_string(),
992 ScalarType::Vector(_) | ScalarType::HalfVector(_) => {
999 "prax_pgvector::filter::VectorFilter".to_string()
1000 }
1001 ScalarType::SparseVector(_) => "ScalarFilter<Vec<(u32, f32)>>".to_string(),
1002 ScalarType::Bit(_) => "ScalarFilter<Vec<u8>>".to_string(),
1003 },
1004 FieldType::Enum(name) => format!("ScalarFilter<{}>", name),
1005 _ => "Filter".to_string(),
1006 }
1007}
1008
1009fn to_snake_case(name: &str) -> String {
1011 let mut result = String::new();
1012 for (i, c) in name.chars().enumerate() {
1013 if c.is_uppercase() {
1014 if i > 0 {
1015 result.push('_');
1016 }
1017 result.push(c.to_lowercase().next().unwrap());
1018 } else {
1019 result.push(c);
1020 }
1021 }
1022 result
1023}
1024
1025fn to_field_ident(name: &str) -> String {
1040 let snake = to_snake_case(name);
1041 if is_rust_keyword(&snake) {
1042 format!("r#{}", snake)
1043 } else {
1044 snake
1045 }
1046}
1047
1048fn is_rust_keyword(s: &str) -> bool {
1049 matches!(
1050 s,
1051 "abstract"
1052 | "as"
1053 | "async"
1054 | "await"
1055 | "become"
1056 | "box"
1057 | "break"
1058 | "const"
1059 | "continue"
1060 | "do"
1061 | "dyn"
1062 | "else"
1063 | "enum"
1064 | "extern"
1065 | "false"
1066 | "final"
1067 | "fn"
1068 | "for"
1069 | "gen"
1070 | "if"
1071 | "impl"
1072 | "in"
1073 | "let"
1074 | "loop"
1075 | "macro"
1076 | "match"
1077 | "mod"
1078 | "move"
1079 | "mut"
1080 | "override"
1081 | "priv"
1082 | "pub"
1083 | "ref"
1084 | "return"
1085 | "static"
1086 | "struct"
1087 | "trait"
1088 | "true"
1089 | "try"
1090 | "type"
1091 | "typeof"
1092 | "unsafe"
1093 | "unsized"
1094 | "use"
1095 | "virtual"
1096 | "where"
1097 | "while"
1098 | "yield"
1099 )
1100}
1101
1102fn to_pascal_case(name: &str) -> String {
1104 if name.is_empty() {
1105 return String::new();
1106 }
1107
1108 let first = name.chars().next().unwrap();
1110 if first.is_uppercase() && name.chars().any(|c| c.is_lowercase()) && !name.contains('_') {
1111 return name.to_string();
1112 }
1113
1114 name.split('_')
1116 .filter(|s| !s.is_empty())
1117 .map(|segment| {
1118 let mut chars = segment.chars();
1119 match chars.next() {
1120 None => String::new(),
1121 Some(first) => {
1122 let rest: String = chars.collect();
1123 format!("{}{}", first.to_uppercase(), rest.to_lowercase())
1124 }
1125 }
1126 })
1127 .collect()
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132 use super::*;
1133
1134 #[test]
1135 fn test_to_snake_case() {
1136 assert_eq!(to_snake_case("BoardMember"), "board_member");
1137 assert_eq!(to_snake_case("User"), "user");
1138 assert_eq!(to_snake_case("JiraImportConfig"), "jira_import_config");
1139 }
1140
1141 #[test]
1142 fn test_to_pascal_case_from_snake() {
1143 assert_eq!(to_pascal_case("card_created"), "CardCreated");
1144 assert_eq!(to_pascal_case("branch_deleted"), "BranchDeleted");
1145 assert_eq!(to_pascal_case("pr_merged"), "PrMerged");
1146 }
1147
1148 #[test]
1149 fn test_to_pascal_case_from_screaming() {
1150 assert_eq!(to_pascal_case("CARD_CREATED"), "CardCreated");
1151 assert_eq!(to_pascal_case("PR_MERGED"), "PrMerged");
1152 }
1153
1154 #[test]
1155 fn test_to_pascal_case_already_pascal() {
1156 assert_eq!(to_pascal_case("Admin"), "Admin");
1157 assert_eq!(to_pascal_case("SuperAdmin"), "SuperAdmin");
1158 assert_eq!(to_pascal_case("Low"), "Low");
1159 }
1160
1161 #[test]
1162 fn test_to_pascal_case_single_word() {
1163 assert_eq!(to_pascal_case("active"), "Active");
1164 assert_eq!(to_pascal_case("ACTIVE"), "Active");
1165 }
1166
1167 #[test]
1168 fn test_needs_boxing_direct_cycle() {
1169 let mut graph = HashMap::new();
1170 graph.insert(
1171 "Board".to_string(),
1172 HashSet::from(["JiraConfig".to_string()]),
1173 );
1174 graph.insert(
1175 "JiraConfig".to_string(),
1176 HashSet::from(["Board".to_string()]),
1177 );
1178
1179 assert!(needs_boxing("Board", "JiraConfig", &graph));
1180 assert!(needs_boxing("JiraConfig", "Board", &graph));
1181 }
1182
1183 #[test]
1184 fn test_needs_boxing_no_cycle() {
1185 let mut graph = HashMap::new();
1186 graph.insert("Post".to_string(), HashSet::from(["User".to_string()]));
1187 graph.insert("User".to_string(), HashSet::new());
1188
1189 assert!(!needs_boxing("Post", "User", &graph));
1190 }
1191
1192 #[test]
1193 fn test_needs_boxing_indirect_cycle() {
1194 let mut graph = HashMap::new();
1195 graph.insert("A".to_string(), HashSet::from(["B".to_string()]));
1196 graph.insert("B".to_string(), HashSet::from(["C".to_string()]));
1197 graph.insert("C".to_string(), HashSet::from(["A".to_string()]));
1198
1199 assert!(needs_boxing("A", "B", &graph));
1200 assert!(needs_boxing("B", "C", &graph));
1201 assert!(needs_boxing("C", "A", &graph));
1202 }
1203}