@@ -339,6 +339,28 @@ fn compute_updates(
339339 . current_req
340340 . trim_start_matches ( |c : char | !c. is_ascii_digit ( ) ) ;
341341
342+ // Safety net: never suggest a downgrade. When both current and selected
343+ // can be parsed as semver, skip this dependency if selected <= current.
344+ // This guards against the registry-filtering path returning an older
345+ // stable when the user is already on a higher prerelease (e.g.
346+ // `sea-orm 2.0.0-rc.37` -> `1.1.20` should NOT be reported).
347+ //
348+ // For ranges like `^1` / `0.6` where we cannot fully parse as semver,
349+ // we fall through to the string-based precision comparison below.
350+ if let ( Ok ( cur_ver) , Ok ( sel_ver) ) = (
351+ semver:: Version :: parse ( current_bare) ,
352+ semver:: Version :: parse ( selected) ,
353+ ) && sel_ver <= cur_ver
354+ {
355+ trace ! (
356+ package = %dep. name,
357+ current = %dep. current_req,
358+ selected = %selected,
359+ "skipping: selected version is not newer than current"
360+ ) ;
361+ continue ;
362+ }
363+
342364 // Preserve precision: if the user wrote "0.6" (2 segments), truncate the
343365 // resolved version to 2 segments before comparing. This respects the user's
344366 // intent to pin only at that granularity.
@@ -894,6 +916,109 @@ mod tests {
894916 assert_eq ! ( updates[ 0 ] . to, "0.25.11" ) ;
895917 }
896918
919+ #[ test]
920+ fn test_compute_updates_blocks_downgrade_from_prerelease_to_stable ( ) {
921+ // Regression test for the sea-orm 2.0.0-rc.37 -> 1.1.20 bug.
922+ // When the current version is a higher prerelease (2.0.0-rc.37) and
923+ // the registry filtering returns an older stable (1.1.20), the
924+ // safety net MUST skip this update instead of suggesting a downgrade.
925+ let deps = vec ! [ DependencySpec {
926+ name: "sea-orm" . to_owned( ) ,
927+ current_req: "2.0.0-rc.37" . to_owned( ) ,
928+ section: DependencySection :: Dependencies ,
929+ } ] ;
930+ let resolved = vec ! [ (
931+ 0 ,
932+ Ok ( ResolvedVersion {
933+ latest: Some ( "1.1.20" . to_owned( ) ) ,
934+ selected: Some ( "1.1.20" . to_owned( ) ) ,
935+ } ) ,
936+ ) ] ;
937+ let updates = compute_updates ( & deps, & resolved) ;
938+ assert ! (
939+ updates. is_empty( ) ,
940+ "must not suggest downgrade from 2.0.0-rc.37 to 1.1.20, got: {updates:?}"
941+ ) ;
942+ }
943+
944+ #[ test]
945+ fn test_compute_updates_blocks_downgrade_same_major ( ) {
946+ // Current is newer stable; registry returned something older. Skip.
947+ let deps = vec ! [ DependencySpec {
948+ name: "pkg" . to_owned( ) ,
949+ current_req: "2.5.0" . to_owned( ) ,
950+ section: DependencySection :: Dependencies ,
951+ } ] ;
952+ let resolved = vec ! [ (
953+ 0 ,
954+ Ok ( ResolvedVersion {
955+ latest: Some ( "2.4.0" . to_owned( ) ) ,
956+ selected: Some ( "2.4.0" . to_owned( ) ) ,
957+ } ) ,
958+ ) ] ;
959+ let updates = compute_updates ( & deps, & resolved) ;
960+ assert ! ( updates. is_empty( ) , "must not downgrade 2.5.0 -> 2.4.0" ) ;
961+ }
962+
963+ #[ test]
964+ fn test_compute_updates_allows_prerelease_to_prerelease_upgrade ( ) {
965+ // Current: 2.0.0-rc.37, Selected: 2.0.0-rc.40 → valid upgrade.
966+ let deps = vec ! [ DependencySpec {
967+ name: "sea-orm" . to_owned( ) ,
968+ current_req: "2.0.0-rc.37" . to_owned( ) ,
969+ section: DependencySection :: Dependencies ,
970+ } ] ;
971+ let resolved = vec ! [ (
972+ 0 ,
973+ Ok ( ResolvedVersion {
974+ latest: Some ( "2.0.0-rc.40" . to_owned( ) ) ,
975+ selected: Some ( "2.0.0-rc.40" . to_owned( ) ) ,
976+ } ) ,
977+ ) ] ;
978+ let updates = compute_updates ( & deps, & resolved) ;
979+ assert_eq ! ( updates. len( ) , 1 ) ;
980+ assert_eq ! ( updates[ 0 ] . to, "2.0.0-rc.40" ) ;
981+ }
982+
983+ #[ test]
984+ fn test_compute_updates_allows_prerelease_to_stable_upgrade ( ) {
985+ // Current: 2.0.0-rc.37 (prerelease), Selected: 2.0.0 (stable) → semver: stable > prerelease of same version.
986+ let deps = vec ! [ DependencySpec {
987+ name: "sea-orm" . to_owned( ) ,
988+ current_req: "2.0.0-rc.37" . to_owned( ) ,
989+ section: DependencySection :: Dependencies ,
990+ } ] ;
991+ let resolved = vec ! [ (
992+ 0 ,
993+ Ok ( ResolvedVersion {
994+ latest: Some ( "2.0.0" . to_owned( ) ) ,
995+ selected: Some ( "2.0.0" . to_owned( ) ) ,
996+ } ) ,
997+ ) ] ;
998+ let updates = compute_updates ( & deps, & resolved) ;
999+ assert_eq ! ( updates. len( ) , 1 ) ;
1000+ assert_eq ! ( updates[ 0 ] . to, "2.0.0" ) ;
1001+ }
1002+
1003+ #[ test]
1004+ fn test_compute_updates_equal_semver_skipped ( ) {
1005+ // Exact same version: must skip (not a "downgrade", but not an upgrade either).
1006+ let deps = vec ! [ DependencySpec {
1007+ name: "pkg" . to_owned( ) ,
1008+ current_req: "1.2.3" . to_owned( ) ,
1009+ section: DependencySection :: Dependencies ,
1010+ } ] ;
1011+ let resolved = vec ! [ (
1012+ 0 ,
1013+ Ok ( ResolvedVersion {
1014+ latest: Some ( "1.2.3" . to_owned( ) ) ,
1015+ selected: Some ( "1.2.3" . to_owned( ) ) ,
1016+ } ) ,
1017+ ) ] ;
1018+ let updates = compute_updates ( & deps, & resolved) ;
1019+ assert ! ( updates. is_empty( ) ) ;
1020+ }
1021+
8971022 #[ test]
8981023 fn test_compute_updates_preserves_section ( ) {
8991024 let deps = vec ! [ DependencySpec {
0 commit comments