Skip to content

Commit 4484fd4

Browse files
authored
Merge pull request #130 from dev-five-git/fix-name-issue
Fix eq issue and username issue
2 parents a38792e + ecbd9c1 commit 4484fd4

5 files changed

Lines changed: 182 additions & 27 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch"},"note":"Fix eq issue and username issue","date":"2026-04-01T11:37:59.029951400Z"}

Cargo.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vespertide-exporter/src/seaorm/mod.rs

Lines changed: 151 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ use std::collections::{HashMap, HashSet};
33
use crate::orm::OrmExporter;
44
use vespertide_config::SeaOrmConfig;
55
use vespertide_core::{
6-
ColumnDef, ColumnType, ComplexColumnType, EnumValues, NumValue, StringOrBool, TableConstraint,
7-
TableDef,
6+
ColumnDef, ColumnType, ComplexColumnType, EnumValues, NumValue, SimpleColumnType, StringOrBool,
7+
TableConstraint, TableDef,
88
};
99

1010
/// Build an absolute `crate::` module path for the target table.
@@ -23,18 +23,30 @@ fn absolute_module_path(crate_prefix: &str, to_module: &[String]) -> String {
2323
}
2424

2525
/// Look up the module path for a table name from the module_paths map.
26-
/// Uses `crate::` absolute paths when crate_prefix and module_paths are available.
27-
/// Falls back to `super::{table_name}` when no mapping exists.
26+
/// Uses `super::` for sibling modules in the same folder, `crate::` absolute paths for
27+
/// cross-directory relations when mappings are available, and falls back to `super::{table_name}`.
2828
fn resolve_entity_module_path(
29+
current_table: &str,
2930
target_table: &str,
3031
module_paths: &HashMap<String, Vec<String>>,
3132
crate_prefix: &str,
3233
) -> String {
33-
if !crate_prefix.is_empty()
34-
&& let Some(to) = module_paths.get(target_table)
35-
{
36-
return absolute_module_path(crate_prefix, to);
34+
if let (Some(current), Some(target)) = (
35+
module_paths.get(current_table),
36+
module_paths.get(target_table),
37+
) {
38+
let current_parent = current.split_last().map_or(&[][..], |(_, parent)| parent);
39+
let target_parent = target.split_last().map_or(&[][..], |(_, parent)| parent);
40+
41+
if current_parent == target_parent {
42+
return format!("super::{target_table}");
43+
}
44+
45+
if !crate_prefix.is_empty() {
46+
return absolute_module_path(crate_prefix, target);
47+
}
3748
}
49+
3850
format!("super::{target_table}")
3951
}
4052

@@ -176,8 +188,13 @@ pub fn render_entity_with_config_and_paths(
176188
}
177189
}
178190

179-
// Build model derive line with optional extra derives
180-
let mut model_derives = vec!["Clone", "Debug", "PartialEq", "Eq", "DeriveEntityModel"];
191+
// Build model derive line with optional extra derives.
192+
// Float-backed fields (f32/f64) cannot implement Eq, so omit it when present.
193+
let mut model_derives = vec!["Clone", "Debug", "PartialEq"];
194+
if table.columns.iter().all(column_supports_eq) {
195+
model_derives.push("Eq");
196+
}
197+
model_derives.push("DeriveEntityModel");
181198
let extra_model_derives: Vec<&str> = config
182199
.extra_model_derives()
183200
.iter()
@@ -416,7 +433,6 @@ fn format_default_value(value: &StringOrBool, column_type: &ColumnType) -> Strin
416433

417434
/// Check if the simple column type is numeric.
418435
fn is_numeric_simple_type(simple: &vespertide_core::SimpleColumnType) -> bool {
419-
use vespertide_core::SimpleColumnType;
420436
matches!(
421437
simple,
422438
SimpleColumnType::SmallInt
@@ -427,6 +443,17 @@ fn is_numeric_simple_type(simple: &vespertide_core::SimpleColumnType) -> bool {
427443
)
428444
}
429445

446+
fn column_supports_eq(column: &ColumnDef) -> bool {
447+
column_type_supports_eq(&column.r#type)
448+
}
449+
450+
fn column_type_supports_eq(column_type: &ColumnType) -> bool {
451+
match column_type {
452+
ColumnType::Simple(SimpleColumnType::Real | SimpleColumnType::DoublePrecision) => false,
453+
ColumnType::Simple(_) | ColumnType::Complex(_) => true,
454+
}
455+
}
456+
430457
fn primary_key_columns(table: &TableDef) -> HashSet<String> {
431458
use vespertide_core::schema::primary_key::PrimaryKeySyntax;
432459
let mut keys = HashSet::new();
@@ -620,7 +647,7 @@ fn relation_field_defs_with_schema(
620647

621648
out.push(attr);
622649
let entity_path =
623-
resolve_entity_module_path(resolved_table, module_paths, crate_prefix);
650+
resolve_entity_module_path(&table.name, resolved_table, module_paths, crate_prefix);
624651
out.push(format!(
625652
" pub {field_name}: HasOne<{entity_path}::Entity>,"
626653
));
@@ -673,6 +700,7 @@ fn generate_relation_enum_name(columns: &[String]) -> String {
673700
/// - FK column: "org_id", table: "user", to: "id" -> "org"
674701
fn infer_field_name_from_fk_column(fk_column: &str, table_name: &str, to: &str) -> String {
675702
let table_lower = table_name.to_lowercase();
703+
let to_lower = to.to_lowercase();
676704

677705
// Remove the "to" suffix from FK column (e.g., "user_id" for to="id", "user_idx" for to="idx").
678706
// If FK column still uses common suffixes like "*_id"/"*_idx", strip them as fallbacks.
@@ -686,6 +714,13 @@ fn infer_field_name_from_fk_column(fk_column: &str, table_name: &str, to: &str)
686714
let sanitized = sanitize_field_name(without_suffix);
687715
let sanitized_lower = sanitized.to_lowercase();
688716

717+
// If the FK column exactly matches the referenced column name, treat it as a natural-key
718+
// relation and expose the target entity name instead of the raw column name.
719+
// Also handle compact forms like `username` for `user.name`.
720+
if sanitized_lower == to_lower || sanitized_lower == format!("{table_lower}{to_lower}") {
721+
return sanitize_field_name(table_name);
722+
}
723+
689724
// If the sanitized name is exactly the table name (e.g., "user_id" -> "user" for table "user"),
690725
// we need to fall back to the table name for proper disambiguation
691726
if sanitized_lower == table_lower {
@@ -978,7 +1013,7 @@ fn reverse_relation_field_defs(
9781013

9791014
out.push(attr);
9801015
let entity_path =
981-
resolve_entity_module_path(&rel.target_entity, module_paths, crate_prefix);
1016+
resolve_entity_module_path(&table.name, &rel.target_entity, module_paths, crate_prefix);
9821017
out.push(format!(
9831018
" pub {field_name}: {rust_type}<{entity_path}::Entity>,"
9841019
));
@@ -1346,23 +1381,42 @@ mod module_path_tests {
13461381
#[test]
13471382
fn resolve_entity_module_path_with_crate_prefix() {
13481383
let mut module_paths = HashMap::new();
1384+
module_paths.insert(
1385+
"estimate".into(),
1386+
vec!["estimate".into(), "estimate".into()],
1387+
);
13491388
module_paths.insert("admin".into(), vec!["admin".into(), "admin".into()]);
1350-
let result = resolve_entity_module_path("admin", &module_paths, "crate::models");
1389+
let result =
1390+
resolve_entity_module_path("estimate", "admin", &module_paths, "crate::models");
13511391
assert_eq!(result, "crate::models::admin::admin");
13521392
}
13531393

1394+
#[test]
1395+
fn resolve_entity_module_path_prefers_super_for_siblings() {
1396+
let mut module_paths = HashMap::new();
1397+
module_paths.insert("admin".into(), vec!["admin".into(), "admin".into()]);
1398+
module_paths.insert(
1399+
"admin_stamp".into(),
1400+
vec!["admin".into(), "admin_stamp".into()],
1401+
);
1402+
1403+
let result =
1404+
resolve_entity_module_path("admin_stamp", "admin", &module_paths, "crate::models");
1405+
assert_eq!(result, "super::admin");
1406+
}
1407+
13541408
#[test]
13551409
fn resolve_entity_module_path_fallback_when_no_mapping() {
13561410
let module_paths = HashMap::new();
1357-
let result = resolve_entity_module_path("user", &module_paths, "crate::models");
1411+
let result = resolve_entity_module_path("post", "user", &module_paths, "crate::models");
13581412
assert_eq!(result, "super::user");
13591413
}
13601414

13611415
#[test]
13621416
fn resolve_entity_module_path_fallback_when_empty_prefix() {
13631417
let mut module_paths = HashMap::new();
13641418
module_paths.insert("admin".into(), vec!["admin".into(), "admin".into()]);
1365-
let result = resolve_entity_module_path("admin", &module_paths, "");
1419+
let result = resolve_entity_module_path("user", "admin", &module_paths, "");
13661420
assert_eq!(result, "super::admin");
13671421
}
13681422
}
@@ -1520,6 +1574,8 @@ mod helper_tests {
15201574
// FK column WITHOUT _id suffix (coverage for line 450)
15211575
#[case("creator_user", "user", "id", "creator_user")]
15221576
#[case("user", "user", "id", "user")]
1577+
#[case("username", "user", "name", "user")]
1578+
#[case("username", "admin", "username", "admin")]
15231579
// FK column exactly matches table name with _id (coverage for line 464)
15241580
#[case("customer_id", "customer", "id", "customer")]
15251581
#[case("product_id", "product", "id", "product")]
@@ -1545,6 +1601,28 @@ mod helper_tests {
15451601
);
15461602
}
15471603

1604+
#[test]
1605+
fn test_column_type_supports_eq() {
1606+
assert!(column_type_supports_eq(&ColumnType::Simple(
1607+
SimpleColumnType::Integer
1608+
)));
1609+
assert!(column_type_supports_eq(&ColumnType::Simple(
1610+
SimpleColumnType::Text
1611+
)));
1612+
assert!(!column_type_supports_eq(&ColumnType::Simple(
1613+
SimpleColumnType::Real
1614+
)));
1615+
assert!(!column_type_supports_eq(&ColumnType::Simple(
1616+
SimpleColumnType::DoublePrecision
1617+
)));
1618+
assert!(column_type_supports_eq(&ColumnType::Complex(
1619+
ComplexColumnType::Numeric {
1620+
precision: 10,
1621+
scale: 2,
1622+
}
1623+
)));
1624+
}
1625+
15481626
#[rstest]
15491627
#[case("hello_world", "HelloWorld")]
15501628
#[case("order_status", "OrderStatus")]
@@ -2730,6 +2808,7 @@ mod tests {
27302808
#[case("not_junction_fk_not_in_pk_other")]
27312809
#[case("not_junction_fk_not_in_pk_another")]
27322810
#[case("multiple_fk_same_table")]
2811+
#[case("username_fk")]
27332812
#[case("multiple_reverse_relations")]
27342813
#[case("multiple_has_one_relations")]
27352814
fn render_entity_with_schema_snapshots(#[case] name: &str) {
@@ -2978,6 +3057,23 @@ mod tests {
29783057
);
29793058
(post.clone(), vec![user, post])
29803059
}
3060+
"username_fk" => {
3061+
let user = table_with_pk(
3062+
"user",
3063+
vec![col("username", ColumnType::Simple(Text))],
3064+
vec!["username"],
3065+
);
3066+
let session = table_with_pk_and_fk(
3067+
"session",
3068+
vec![
3069+
col("id", ColumnType::Simple(Uuid)),
3070+
col("username", ColumnType::Simple(Text)),
3071+
],
3072+
vec!["id"],
3073+
vec![(vec!["username"], "user", vec!["username"])],
3074+
);
3075+
(session.clone(), vec![user, session])
3076+
}
29813077
"multiple_reverse_relations" => {
29823078
// Test case where user has multiple has_one relations from profile
29833079
let user = table_with_pk(
@@ -3077,6 +3173,45 @@ mod tests {
30773173
assert!(rendered.contains("default_value = 0.00"));
30783174
}
30793175

3176+
#[test]
3177+
fn render_entity_omits_eq_for_float_models() {
3178+
use vespertide_core::schema::primary_key::PrimaryKeySyntax;
3179+
3180+
let table = TableDef {
3181+
name: "measurements".into(),
3182+
description: None,
3183+
columns: vec![
3184+
ColumnDef {
3185+
name: "id".into(),
3186+
r#type: ColumnType::Simple(SimpleColumnType::Integer),
3187+
nullable: false,
3188+
default: None,
3189+
comment: None,
3190+
primary_key: Some(PrimaryKeySyntax::Bool(true)),
3191+
unique: None,
3192+
index: None,
3193+
foreign_key: None,
3194+
},
3195+
ColumnDef {
3196+
name: "score".into(),
3197+
r#type: ColumnType::Simple(SimpleColumnType::DoublePrecision),
3198+
nullable: false,
3199+
default: None,
3200+
comment: None,
3201+
primary_key: None,
3202+
unique: None,
3203+
index: None,
3204+
foreign_key: None,
3205+
},
3206+
],
3207+
constraints: vec![],
3208+
};
3209+
3210+
let rendered = render_entity(&table);
3211+
assert!(rendered.contains("#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]"));
3212+
assert!(!rendered.contains("#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]"));
3213+
}
3214+
30803215
#[test]
30813216
fn test_orm_exporter_trait() {
30823217
use crate::orm::OrmExporter;

crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_table_level_pk.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ expression: rendered
55
use sea_orm::entity::prelude::*;
66

77
#[sea_orm::model]
8-
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
8+
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
99
#[sea_orm(table_name = "orders")]
1010
pub struct Model {
1111
#[sea_orm(primary_key)]

0 commit comments

Comments
 (0)