1use std::collections::{HashMap, HashSet};
2
3use dinoco_compiler::{
4 ParsedField, ParsedFieldDefault, ParsedFieldType, ParsedRelation, ParsedSchema, ParsedTable, ReferentialAction,
5};
6
7use crate::{MigrationPlan, MigrationStep, SafetyLevel, is_destructive_cast};
8
9fn primary_key_type(table: &ParsedTable, field_name: &str) -> Option<ParsedFieldType> {
10 table.fields.iter().find(|field| field.name == field_name).map(|field| field.field_type.clone())
11}
12
13fn primary_key_constraint_name(table_name: &str) -> Option<String> {
14 Some(format!("pk_{}", table_name))
15}
16
17fn push_safety_alert(alerts: &mut Vec<SafetyLevel>, alert: SafetyLevel) {
18 let exists = alerts.iter().any(|item| match (item, &alert) {
19 (SafetyLevel::Warning(left), SafetyLevel::Warning(right)) => left == right,
20 (SafetyLevel::Destructive(left), SafetyLevel::Destructive(right)) => left == right,
21 _ => false,
22 });
23
24 if !exists {
25 alerts.push(alert);
26 }
27}
28
29fn diff_primary_key(
30 old_table: &ParsedTable,
31 new_table: &ParsedTable,
32 alerts: &mut Vec<SafetyLevel>,
33) -> Vec<MigrationStep> {
34 if old_table.primary_key_fields == new_table.primary_key_fields {
35 return Vec::new();
36 }
37
38 let old_primary_key = if old_table.primary_key_fields.is_empty() {
39 "(none)".to_string()
40 } else {
41 old_table.primary_key_fields.join(", ")
42 };
43 let new_primary_key = if new_table.primary_key_fields.is_empty() {
44 "(none)".to_string()
45 } else {
46 new_table.primary_key_fields.join(", ")
47 };
48
49 push_safety_alert(
50 alerts,
51 SafetyLevel::Destructive(format!(
52 "Primary key changed on table '{}': [{}] -> [{}]. This can fail if existing data violates the new primary key or dependent relations still use the old key.",
53 new_table.database_name, old_primary_key, new_primary_key
54 )),
55 );
56
57 let mut steps = Vec::new();
58
59 if !old_table.primary_key_fields.is_empty() {
60 steps.push(MigrationStep::DropPrimaryKey {
61 table_name: old_table.database_name.clone(),
62 constraint_name: primary_key_constraint_name(&old_table.database_name),
63 });
64 }
65
66 if !new_table.primary_key_fields.is_empty() {
67 steps.push(MigrationStep::AddPrimaryKey {
68 table_name: new_table.database_name.clone(),
69 columns: new_table.primary_key_fields.clone(),
70 constraint_name: primary_key_constraint_name(&new_table.database_name),
71 });
72 }
73
74 steps
75}
76
77fn explicit_index_name(table_name: &str, columns: &[String], is_unique: bool) -> String {
78 let suffix = columns.join("_");
79
80 if is_unique { format!("uq_{}_{}", table_name, suffix) } else { format!("idx_{}_{}", table_name, suffix) }
81}
82
83fn diff_table_indexes(old_table: &ParsedTable, new_table: &ParsedTable) -> Vec<MigrationStep> {
84 let old_unique = old_table.unique_field_sets.iter().map(|columns| (true, columns.clone())).collect::<Vec<_>>();
85 let old_indexes = old_table.index_field_sets.iter().map(|columns| (false, columns.clone())).collect::<Vec<_>>();
86 let new_unique = new_table.unique_field_sets.iter().map(|columns| (true, columns.clone())).collect::<Vec<_>>();
87 let new_indexes = new_table.index_field_sets.iter().map(|columns| (false, columns.clone())).collect::<Vec<_>>();
88
89 let old_all = old_unique.into_iter().chain(old_indexes).collect::<Vec<_>>();
90 let new_all = new_unique.into_iter().chain(new_indexes).collect::<Vec<_>>();
91 let old_signatures = old_all
92 .iter()
93 .map(|(is_unique, columns)| ((*is_unique, columns.join("|")), columns.clone()))
94 .collect::<HashMap<_, _>>();
95 let new_signatures = new_all
96 .iter()
97 .map(|(is_unique, columns)| ((*is_unique, columns.join("|")), columns.clone()))
98 .collect::<HashMap<_, _>>();
99 let mut steps = Vec::new();
100
101 for ((is_unique, _), columns) in &old_signatures {
102 if !new_signatures.contains_key(&(*is_unique, columns.join("|"))) {
103 steps.push(MigrationStep::DropIndex {
104 table_name: old_table.database_name.clone(),
105 index_name: explicit_index_name(&old_table.database_name, columns, *is_unique),
106 });
107 }
108 }
109
110 for ((is_unique, _), columns) in &new_signatures {
111 if !old_signatures.contains_key(&(*is_unique, columns.join("|"))) {
112 steps.push(MigrationStep::CreateIndex {
113 table_name: new_table.database_name.clone(),
114 columns: columns.clone(),
115 index_name: explicit_index_name(&new_table.database_name, columns, *is_unique),
116 is_unique: *is_unique,
117 });
118 }
119 }
120
121 steps
122}
123
124pub fn calculate_diff(old_schema: &Option<ParsedSchema>, new_schema: &ParsedSchema) -> MigrationPlan {
125 let fallback_old_schema = ParsedSchema { config: new_schema.config.clone(), enums: Vec::new(), tables: Vec::new() };
126 let old_schema = old_schema.as_ref().unwrap_or(&fallback_old_schema);
127
128 let mut safety_alerts = Vec::new();
129
130 let mut create_enum_steps = Vec::new();
131 let mut alter_enum_steps = Vec::new();
132 let mut drop_enum_steps = Vec::new();
133 let mut drop_fk_steps = Vec::new();
134 let mut drop_index_steps = Vec::new();
135 let mut drop_table_steps = Vec::new();
136 let mut create_table_steps = Vec::new();
137 let mut add_column_steps = Vec::new();
138 let mut drop_column_steps = Vec::new();
139 let mut alter_column_steps = Vec::new();
140 let mut primary_key_steps = Vec::new();
141 let mut create_index_steps = Vec::new();
142 let mut add_fk_steps = Vec::new();
143 let mut created_table_names = HashSet::new();
144
145 let old_enums_map: HashMap<&String, _> = old_schema.enums.iter().map(|e| (&e.name, e)).collect();
146 let new_enums_map: HashMap<&String, _> = new_schema.enums.iter().map(|e| (&e.name, e)).collect();
147
148 for (name, new_enum) in &new_enums_map {
149 if let Some(old_enum) = old_enums_map.get(name) {
150 if old_enum.values != new_enum.values {
151 alter_enum_steps.push(MigrationStep::AlterEnum {
152 name: (*name).clone(),
153 old_variants: old_enum.values.clone(),
154 new_variants: new_enum.values.clone(),
155 });
156 }
157 } else {
158 create_enum_steps
159 .push(MigrationStep::CreateEnum { name: (*name).clone(), variants: new_enum.values.clone() });
160 }
161 }
162
163 for name in old_enums_map.keys() {
164 if !new_enums_map.contains_key(name) {
165 drop_enum_steps.push(MigrationStep::DropEnum((*name).clone()));
166 }
167 }
168
169 let old_map: HashMap<&String, &ParsedTable> = old_schema.tables.iter().map(|t| (&t.name, t)).collect();
170 let new_map: HashMap<&String, &ParsedTable> = new_schema.tables.iter().map(|t| (&t.name, t)).collect();
171
172 let mut old_join_tables = HashMap::new();
173 for table in &old_schema.tables {
174 let (_, jts) = extract_relations(None, table, &old_schema.tables);
175 for jt in jts {
176 old_join_tables.insert(jt.name.clone(), jt);
177 }
178 }
179
180 let mut new_join_tables_map = HashMap::new();
181
182 for (name, new_table) in &new_map {
183 if let Some(old_table) = old_map.get(name) {
184 if old_table.database_name != new_table.database_name {
185 push_safety_alert(
186 &mut safety_alerts,
187 SafetyLevel::Warning(format!(
188 "Table '{}' will be renamed to '{}'. Existing queries or raw SQL using the old physical table name may need updates.",
189 old_table.database_name, new_table.database_name
190 )),
191 );
192 created_table_names.insert(new_table.database_name.clone());
193 create_table_steps.push(MigrationStep::RenameTable {
194 old_name: old_table.database_name.clone(),
195 new_name: new_table.database_name.clone(),
196 });
197 }
198
199 primary_key_steps.extend(diff_primary_key(old_table, new_table, &mut safety_alerts));
200 for step in diff_table_indexes(old_table, new_table) {
201 match step {
202 MigrationStep::DropIndex { .. } => drop_index_steps.push(step),
203 MigrationStep::CreateIndex { .. } => create_index_steps.push(step),
204 _ => {}
205 }
206 }
207
208 for step in diff_columns(old_table, new_table, &mut safety_alerts) {
209 match step {
210 MigrationStep::AddColumn { .. } => add_column_steps.push(step),
211 MigrationStep::DropColumn { .. } => drop_column_steps.push(step),
212 MigrationStep::DropForeignKey { .. } => drop_fk_steps.push(step),
213 MigrationStep::AlterColumn { .. } | MigrationStep::RenameColumn { .. } => {
214 alter_column_steps.push(step)
215 }
216 _ => {}
217 }
218 }
219 } else {
220 create_table_steps.push(MigrationStep::CreateTable((*new_table).clone()));
221 created_table_names.insert(new_table.database_name.clone());
222 }
223
224 let (relations_steps, join_tables) =
225 extract_relations(old_map.get(name).copied(), new_table, &new_schema.tables);
226
227 for step in relations_steps {
228 match step {
229 MigrationStep::AddForeignKey { ref table_name, .. } => {
230 if !created_table_names.contains(table_name) {
231 add_fk_steps.push(step);
232 }
233 }
234 MigrationStep::DropForeignKey { .. } => drop_fk_steps.push(step),
235 MigrationStep::CreateIndex { .. } => create_index_steps.push(step),
236 _ => {}
237 }
238 }
239
240 for join_table in join_tables {
241 new_join_tables_map.insert(join_table.name.clone(), join_table.clone());
242
243 if !old_map.contains_key(&join_table.name) && !old_join_tables.contains_key(&join_table.name) {
244 created_table_names.insert(join_table.database_name.clone());
245 create_table_steps.push(MigrationStep::CreateTable(join_table));
246 }
247 }
248 }
249
250 for (name, old_table) in &old_map {
251 if !new_map.contains_key(*name) {
252 for field in &old_table.fields {
253 if let ParsedRelation::ManyToOne(_, local_cols, _, _, _)
254 | ParsedRelation::OneToOneOwner(_, local_cols, _, _, _) = &field.relation
255 {
256 if !local_cols.is_empty() {
257 drop_fk_steps.push(MigrationStep::DropForeignKey {
258 table_name: old_table.database_name.clone(),
259 constraint_name: format!("fk_{}_{}", old_table.database_name, local_cols.join("_")),
260 });
261 }
262 }
263 }
264 }
265 }
266
267 for name in old_join_tables.keys() {
268 if !new_join_tables_map.contains_key(name) {
269 drop_table_steps.push(MigrationStep::DropTable(name.clone()));
270 }
271 }
272
273 for (name, old_table) in &old_map {
274 if !new_map.contains_key(name) {
275 safety_alerts.push(SafetyLevel::Destructive(format!(
276 "Dropping table '{}'. All records will be permanently deleted.",
277 old_table.database_name
278 )));
279
280 drop_table_steps.push(MigrationStep::DropTable(old_table.database_name.clone()));
281 }
282 }
283
284 let mut final_steps = Vec::new();
285
286 final_steps.extend(create_enum_steps);
287 final_steps.extend(drop_fk_steps);
288 final_steps.extend(drop_index_steps);
289 final_steps.extend(drop_table_steps);
290 final_steps.extend(alter_enum_steps);
291 final_steps.extend(create_table_steps);
292 final_steps.extend(drop_column_steps);
293 final_steps.extend(add_column_steps);
294 final_steps.extend(alter_column_steps);
295 final_steps.extend(primary_key_steps);
296 final_steps.extend(create_index_steps);
297 final_steps.extend(add_fk_steps);
298 final_steps.extend(drop_enum_steps);
299
300 MigrationPlan { steps: final_steps, safety_alerts }
301}
302fn diff_columns(old_table: &ParsedTable, new_table: &ParsedTable, alerts: &mut Vec<SafetyLevel>) -> Vec<MigrationStep> {
303 let mut steps = Vec::new();
304
305 let old_fields: HashMap<&String, &ParsedField> =
306 old_table.fields.iter().filter(|f| !f.is_virtual).map(|f| (&f.name, f)).collect();
307 let new_fields: HashMap<&String, &ParsedField> =
308 new_table.fields.iter().filter(|f| !f.is_virtual).map(|f| (&f.name, f)).collect();
309
310 let mut added_fields = Vec::new();
311 let mut dropped_fields = Vec::new();
312
313 for (name, new_field) in &new_fields {
314 if matches!(new_field.field_type, ParsedFieldType::Relation(..)) {
315 continue;
316 }
317
318 if let Some(old_field) = old_fields.get(name) {
319 let type_changed = old_field.field_type != new_field.field_type;
320 let became_required = old_field.is_optional && !new_field.is_optional;
321 let default_value = old_field.default_value != new_field.default_value;
322 let unique = old_field.is_unique != new_field.is_unique;
323
324 if type_changed || became_required || default_value || unique {
325 if type_changed && is_destructive_cast(&old_field.field_type, &new_field.field_type) {
326 push_safety_alert(
327 alerts,
328 SafetyLevel::Destructive(format!(
329 "Incompatible type change in '{}.{}': {:?} -> {:?}. Existing values may not be convertible.",
330 new_table.database_name, name, old_field.field_type, new_field.field_type
331 )),
332 );
333 }
334
335 if became_required && new_field.default_value == ParsedFieldDefault::NotDefined {
336 push_safety_alert(
337 alerts,
338 SafetyLevel::Warning(format!(
339 "Column '{}.{}' was changed from optional to required without a default value. This migration can fail if existing rows contain NULL.",
340 new_table.database_name, name
341 )),
342 );
343 }
344
345 steps.push(MigrationStep::AlterColumn {
346 table_name: new_table.database_name.clone(),
347 old_field: (*old_field).clone(),
348 new_field: (*new_field).clone(),
349 });
350 }
351 } else {
352 added_fields.push(*new_field);
353 }
354 }
355
356 for (name, old_field) in &old_fields {
357 if matches!(old_field.field_type, ParsedFieldType::Relation(..)) {
358 continue;
359 }
360
361 if !new_fields.contains_key(name) {
362 dropped_fields.push(*old_field);
363 }
364 }
365
366 let mut resolved_adds = HashSet::new();
367 let mut resolved_drops = HashSet::new();
368
369 for add_f in added_fields.iter() {
370 if let Some(drop_f) = dropped_fields.iter().find(|d| {
371 !resolved_drops.contains(&d.name)
372 && d.field_type == add_f.field_type
373 && d.is_optional == add_f.is_optional
374 && d.default_value == add_f.default_value
375 && d.relation == add_f.relation
376 }) {
377 steps.push(MigrationStep::RenameColumn {
378 table_name: new_table.database_name.clone(),
379 old_name: drop_f.name.clone(),
380 new_name: add_f.name.clone(),
381 });
382
383 if let ParsedRelation::ManyToOne(_, local_cols, _, _, _)
384 | ParsedRelation::OneToOneOwner(_, local_cols, _, _, _) = &drop_f.relation
385 {
386 if !local_cols.is_empty() {
387 steps.push(MigrationStep::DropForeignKey {
388 table_name: old_table.database_name.clone(),
389 constraint_name: format!("fk_{}_{}", old_table.database_name, local_cols.join("_")),
390 });
391 }
392 }
393
394 resolved_adds.insert(add_f.name.clone());
395 resolved_drops.insert(drop_f.name.clone());
396 }
397 }
398
399 for add_f in added_fields {
400 if !resolved_adds.contains(&add_f.name) {
401 if !add_f.is_optional
402 && add_f.default_value == ParsedFieldDefault::NotDefined
403 && old_table
404 .fields
405 .iter()
406 .any(|field| !field.is_virtual && !matches!(field.field_type, ParsedFieldType::Relation(..)))
407 {
408 push_safety_alert(
409 alerts,
410 SafetyLevel::Warning(format!(
411 "Adding required column '{}.{}' without a default value can fail on tables that already contain rows.",
412 new_table.database_name, add_f.name
413 )),
414 );
415 }
416
417 steps.push(MigrationStep::AddColumn { table_name: new_table.database_name.clone(), field: add_f.clone() });
418 }
419 }
420
421 for drop_f in dropped_fields {
422 if !resolved_drops.contains(&drop_f.name) {
423 if let ParsedRelation::ManyToOne(_, local_cols, _, _, _)
424 | ParsedRelation::OneToOneOwner(_, local_cols, _, _, _) = &drop_f.relation
425 {
426 if !local_cols.is_empty() {
427 steps.push(MigrationStep::DropForeignKey {
428 table_name: old_table.database_name.clone(),
429 constraint_name: format!("fk_{}_{}", old_table.database_name, local_cols.join("_")),
430 });
431 }
432 }
433
434 push_safety_alert(
435 alerts,
436 SafetyLevel::Destructive(format!(
437 "Dropping column '{}.{}'. All data stored in this column will be permanently lost.",
438 old_table.database_name, drop_f.name
439 )),
440 );
441
442 steps
443 .push(MigrationStep::DropColumn { table_name: old_table.database_name.clone(), field: drop_f.clone() });
444 }
445 }
446
447 steps
448}
449
450pub fn extract_relations(
451 old_table: Option<&ParsedTable>,
452 new_table: &ParsedTable,
453 all_tables: &[ParsedTable],
454) -> (Vec<MigrationStep>, Vec<ParsedTable>) {
455 let mut fk_steps = Vec::new();
456 let mut join_tables = Vec::new();
457 let tables_by_name: HashMap<&str, &ParsedTable> =
458 all_tables.iter().map(|table| (table.name.as_str(), table)).collect();
459
460 let mut processed_m2m = HashSet::new();
461
462 for field in &new_table.fields {
463 let is_unchanged = if let Some(old_t) = old_table {
464 if let Some(old_f) = old_t.fields.iter().find(|f| f.name == field.name) {
465 old_f.relation == field.relation && old_f.field_type == field.field_type
466 } else {
467 false
468 }
469 } else {
470 false
471 };
472
473 match &field.relation {
474 ParsedRelation::ManyToOne(_, local_cols, ref_cols, on_delete, on_update)
475 | ParsedRelation::OneToOneOwner(_, local_cols, ref_cols, on_delete, on_update) => {
476 if is_unchanged {
477 continue;
478 }
479
480 if !local_cols.is_empty() && local_cols.len() == ref_cols.len() {
481 let ref_table = match &field.field_type {
482 ParsedFieldType::Relation(name) => name.clone(),
483 _ => field.field_type.to_string(),
484 };
485
486 fk_steps.push(MigrationStep::AddForeignKey {
487 table_name: new_table.database_name.clone(),
488 columns: local_cols.clone(),
489 referenced_table: tables_by_name
490 .get(ref_table.as_str())
491 .map(|table| table.database_name.clone())
492 .unwrap_or(ref_table),
493 referenced_columns: ref_cols.clone(),
494 on_delete: on_delete.clone(),
495 on_update: on_update.clone(),
496 constraint_name: format!("fk_{}_{}", new_table.database_name, local_cols.join("_")),
497 });
498 }
499 }
500 ParsedRelation::ManyToMany(relation_name) => {
501 let target_table_name = match &field.field_type {
502 ParsedFieldType::Relation(name) => name.clone(),
503 _ => continue,
504 };
505
506 if new_table.name <= target_table_name {
507 let Some(safe_rel_name) = build_many_to_many_join_table_name(
508 new_table,
509 field,
510 &target_table_name,
511 relation_name.as_deref(),
512 all_tables,
513 ) else {
514 continue;
515 };
516
517 if !processed_m2m.insert(safe_rel_name.clone()) {
518 continue;
519 }
520
521 let t1_clean = new_table.database_name.replace("\"", "").to_lowercase();
522 let t2_clean = tables_by_name
523 .get(target_table_name.as_str())
524 .map(|table| table.database_name.replace("\"", "").to_lowercase())
525 .unwrap_or_else(|| target_table_name.replace("\"", "").to_lowercase());
526
527 let target_table = tables_by_name.get(target_table_name.as_str()).copied();
528 let current_primary_keys = new_table.primary_key_fields.clone();
529 let target_primary_keys =
530 target_table.map(|table| table.primary_key_fields.clone()).unwrap_or_default();
531
532 let self_relation = new_table.name == target_table_name;
533 let current_join_columns = current_primary_keys
534 .iter()
535 .map(|field_name| {
536 if self_relation {
537 format!("{}_A_{}", t1_clean, field_name)
538 } else {
539 format!("{}_{}", t1_clean, field_name)
540 }
541 })
542 .collect::<Vec<_>>();
543 let target_join_columns = target_primary_keys
544 .iter()
545 .map(|field_name| {
546 if self_relation {
547 format!("{}_B_{}", t1_clean, field_name)
548 } else {
549 format!("{}_{}", t2_clean, field_name)
550 }
551 })
552 .collect::<Vec<_>>();
553
554 let mut join_fields = Vec::new();
555 for (column_name, field_name) in current_join_columns.iter().zip(current_primary_keys.iter()) {
556 join_fields.push(ParsedField {
557 name: column_name.clone(),
558 field_type: primary_key_type(new_table, field_name).unwrap_or(ParsedFieldType::Integer),
559 is_primary_key: false,
560 is_optional: false,
561 is_unique: false,
562 is_virtual: false,
563 is_list: false,
564 relation: ParsedRelation::NotDefined,
565 default_value: ParsedFieldDefault::NotDefined,
566 });
567 }
568
569 if let Some(target_table) = target_table {
570 for (column_name, field_name) in target_join_columns.iter().zip(target_primary_keys.iter()) {
571 join_fields.push(ParsedField {
572 name: column_name.clone(),
573 field_type: primary_key_type(target_table, field_name)
574 .unwrap_or(ParsedFieldType::Integer),
575 is_primary_key: false,
576 is_optional: false,
577 is_unique: false,
578 is_virtual: false,
579 is_list: false,
580 relation: ParsedRelation::NotDefined,
581 default_value: ParsedFieldDefault::NotDefined,
582 });
583 }
584 }
585
586 let join_table = ParsedTable {
587 name: safe_rel_name.clone(),
588 database_name: safe_rel_name.clone(),
589 primary_key_fields: current_join_columns
590 .iter()
591 .chain(target_join_columns.iter())
592 .cloned()
593 .collect(),
594 unique_field_sets: Vec::new(),
595 index_field_sets: Vec::new(),
596 fields: join_fields,
597 };
598
599 join_tables.push(join_table.clone());
600
601 if is_unchanged {
602 continue;
603 }
604
605 fk_steps.push(MigrationStep::AddForeignKey {
606 table_name: safe_rel_name.clone(),
607 columns: current_join_columns.clone(),
608 referenced_table: new_table.database_name.clone(),
609 referenced_columns: new_table.primary_key_fields.clone(),
610 on_delete: Some(ReferentialAction::Cascade),
611 on_update: Some(ReferentialAction::Cascade),
612 constraint_name: format!("fk_{}_{}", safe_rel_name, current_join_columns.join("_")),
613 });
614
615 fk_steps.push(MigrationStep::AddForeignKey {
616 table_name: safe_rel_name.clone(),
617 columns: target_join_columns.clone(),
618 referenced_table: tables_by_name
619 .get(target_table_name.as_str())
620 .map(|table| table.database_name.clone())
621 .unwrap_or(target_table_name.clone()),
622 referenced_columns: tables_by_name
623 .get(target_table_name.as_str())
624 .map(|table| table.primary_key_fields.clone())
625 .unwrap_or_else(|| vec!["id".to_string()]),
626 on_delete: Some(ReferentialAction::Cascade),
627 on_update: Some(ReferentialAction::Cascade),
628 constraint_name: format!("fk_{}_{}", safe_rel_name, target_join_columns.join("_")),
629 });
630
631 let index_name =
632 format!("{}_{}_idx", safe_rel_name.replace("\"", ""), target_join_columns.join("_"));
633
634 fk_steps.push(MigrationStep::CreateIndex {
635 table_name: safe_rel_name.clone(),
636 columns: target_join_columns,
637 index_name,
638 is_unique: false,
639 });
640 }
641 }
642 _ => {}
643 }
644 }
645
646 (fk_steps, join_tables)
647}
648
649fn build_many_to_many_join_table_name(
650 current_table: &ParsedTable,
651 current_field: &ParsedField,
652 target_table_name: &str,
653 relation_name: Option<&str>,
654 all_tables: &[ParsedTable],
655) -> Option<String> {
656 if let Some(relation_name) = relation_name {
657 return Some(format!("_{}", relation_name.replace('"', "")));
658 }
659
660 if current_table.name == target_table_name {
661 let anchor_field =
662 find_many_to_many_counterpart(current_table, current_field, target_table_name, relation_name, all_tables)
663 .map(|field| if current_field.name <= field.name { current_field } else { field })
664 .unwrap_or(current_field);
665
666 return Some(format!("_{}{}", current_table.name, pascal_case(&anchor_field.name)));
667 }
668
669 if current_table.name.as_str() < target_table_name {
670 return Some(format!("_{}{}", current_table.name, pascal_case(¤t_field.name)));
671 }
672
673 let counterpart =
674 find_many_to_many_counterpart(current_table, current_field, target_table_name, relation_name, all_tables)?;
675
676 Some(format!("_{}{}", target_table_name, pascal_case(&counterpart.name)))
677}
678
679fn find_many_to_many_counterpart<'a>(
680 current_table: &ParsedTable,
681 current_field: &ParsedField,
682 target_table_name: &str,
683 relation_name: Option<&str>,
684 all_tables: &'a [ParsedTable],
685) -> Option<&'a ParsedField> {
686 let target_table = all_tables.iter().find(|table| table.name == target_table_name)?;
687
688 target_table.fields.iter().find(|candidate| {
689 if !matches!(candidate.field_type, ParsedFieldType::Relation(ref target) if target == ¤t_table.name) {
690 return false;
691 }
692
693 if current_table.name == target_table_name && candidate.name == current_field.name {
694 return false;
695 }
696
697 matches!(
698 &candidate.relation,
699 ParsedRelation::ManyToMany(candidate_relation_name)
700 if candidate_relation_name.as_deref() == relation_name
701 )
702 })
703}
704
705fn pascal_case(value: &str) -> String {
706 let mut output = String::new();
707 let mut uppercase_next = true;
708
709 for ch in value.chars() {
710 if ch == '_' || ch == '-' || ch == ' ' {
711 uppercase_next = true;
712 continue;
713 }
714
715 if uppercase_next {
716 output.extend(ch.to_uppercase());
717 uppercase_next = false;
718 } else {
719 output.push(ch);
720 }
721 }
722
723 output
724}