## Release Notes: SeaORM 2.0.0-rc.41 *(since 2.0.0-rc.40)* ### New Features **`SelectFourMany` with `consolidate()`** (#3054) `SelectFour::consolidate()` returns a `SelectFourMany`; its `all()` (star topology) rolls the three has-many children up into `Vec`s — `Vec<(E::Model, Vec, Vec, Vec)>` — mirroring `SelectThreeMany` instead of returning flat `Option` tuples. ```rust let rows = parent::Entity::find() .find_also_related(a::Entity) .find_also_related(b::Entity) .find_also(parent::Entity, c::Entity) .consolidate() .all(db) .await?; ``` **Update without returning** (#2965) `ActiveModelTrait::update_without_returning` (and `UpdateOne` / `ValidatedUpdateOne::exec_without_returning`) run an UPDATE without a RETURNING clause, yielding an `UpdateResult` — useful on backends without RETURNING or when the model isn't needed. It runs `before_save`, skips `after_save`, and returns `DbErr::RecordNotUpdated` when no row matches. ```rust let res = active_model.update_without_returning(db).await?; ``` **Explicit prefixes for nested result models** (#2989) `FromQueryResult` and `DerivePartialModel` accept `#[sea_orm(nested(prefix = "..."))]`, so a model can hold several nested fields of the same type without column clashes: ```rust #[sea_orm(nested(prefix = "mgr_"), alias = "manager")] manager: Worker, #[sea_orm(nested(prefix = "csh_"), alias = "cashier")] cashier: Worker, ``` **Migration CLI custom connection entrypoints** (#3035) `sea-orm-migration` exposes custom connection entrypoints so applications can customize `ConnectOptions`, load SQLite extensions, or build the connection from their own config before running migration commands. ```rust run_cli_with_connection(migrator, |mut opt: ConnectOptions| async move { opt.sqlx_logging(false); // ...or load extensions, read app config, etc. Database::connect(opt).await }) .await; ``` **Prebuilt `sea-orm-cli` binaries via `cargo-binstall`** (#2877) Releases now publish prebuilt `sea-orm-cli` binaries for Linux (x64/arm64), macOS (x64/arm64), and Windows (x64), and the crate carries `cargo-binstall` metadata, so `cargo binstall sea-orm-cli` installs without compiling from source. ### Enhancements **Stable serde for first-party value wrappers** (#3023) First-party wrappers now have explicit serde impls instead of relying on derive shape: `TextUuid` is a JSON string, and the Unix-timestamp wrappers (`ChronoUnixTimestamp`, `ChronoUnixTimestampMillis`, `TimeUnixTimestamp`, `TimeUnixTimestampMillis`) are JSON `i64`s matching their stored value. ```rust let json = serde_json::to_string(&TextUuid::from(uuid))?; // "\"\"" serde_json::from_str::("\"not-a-uuid\"").is_err(); // parse-checked ``` **Streaming behind `stream` cargo feature** (#3075) Streaming APIs (`QueryStream`, `StreamTrait`, `Select::stream*`) are gated behind the `stream` feature, on by default. `default-features = false` without `stream` drops the `ouroboros` dependency. ```toml # opt out of streaming (and ouroboros) by dropping default features sea-orm = { version = "2.0.0-rc.41", default-features = false, features = ["macros", "sqlx-postgres", "runtime-tokio-native-tls"] } ``` **Generated migration names** (#2580) Generated migrations implement `MigrationName` explicitly, keeping names stable when the file/module layout is customized. ### Bug Fixes **`ActiveModelBehavior` runs for junction tables** (#3027, #3010) When establishing a many-to-many relation through the ActiveModel builder, the junction-table rows now run their `ActiveModelBehavior` hooks (`before_save` / `after_save` on inserted via-models, `before_delete` / `after_delete` on removed ones) instead of being inserted/deleted without hooks. **Generated entities compile when an enum name collides with a generated identifier** (#2880, #2153) An enum named like a generated item (`Model`, `Entity`, `ActiveModel`, `Column`, `PrimaryKey`, `Relation`) is now imported aliased instead of emitting a conflicting `use`: ```rust use super::sea_orm_active_enums::Model as ModelEnum; ``` **`serde(rename_all)` no longer breaks `ActiveModel::from_json` defaulting** (#2855, #2854) Regression from #2842: with `#[serde(rename_all = "...")]` (or field-level `rename`), missing fields errored instead of defaulting to `ActiveValue::NotSet`. The default-merge is now keyed off each column's `json_key()`, which respects both rename forms. ```rust #[serde(rename_all = "camelCase")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, pub first_name: String } // `id` omitted → NotSet again (previously errored "missing field") let am = ActiveModel::from_json(json!({ "firstName": "Max" }))?; assert_eq!(am.id, ActiveValue::NotSet); ``` **Entity loader diamond relations** (#3030) The entity loader distinguishes multiple relations to the same target by their relation definition rather than table reference, fixing diamond graphs where two fields point to the same entity through different foreign keys: ```rust #[sea_orm(belongs_to, from = "sender_id", to = "id")] pub sender: HasOne, #[sea_orm(belongs_to, from = "recipient_id", to = "id")] pub recipient: HasOne, // both → user, now loaded independently ``` **Schema-sync indexes for non-default PostgreSQL schemas** (#3085, #3084) On PostgreSQL, indexes from `#[sea_orm(indexed)]` / `#[sea_orm(unique_key)]` on a `schema_name` entity now target the qualified table (`ON "sys"."app_user"`), fixing `relation "..." does not exist` and wrong-schema targeting. MySQL/SQLite keep unqualified targets (their SeaQuery index builders reject a qualified one), which also removes a latent panic for `#[sea_orm(unique)]` columns there. **CLI extra attribute parsing** (#3032) Entity generation handles comma-separated extra attributes and derives with nested commas: ```shell --model-extra-attributes 'serde(rename_all = "camelCase"),ts(export)' ``` **PostgreSQL `drop_everything` with dependent custom types** (#3014) `drop_everything` drops PostgreSQL custom types with `CASCADE`, cleaning up dependent domains and functions while preserving extension-owned types. **Generated `delete_by_*` for single-column unique keys** (#3059) `model_ex` generates `delete_by_*` helpers for one-column unique keys, matching the existing `find_by_*` behavior for `#[sea_orm(unique)]` and single-column `unique_key` fields. ```rust Entity::delete_by_name("alpha").exec(db).await?; // alongside find_by_name ``` **SQLite file URI parameters** (#3072) Rusqlite connections allow SQLite file URI query parameters (e.g. shared in-memory databases), leaving validation to SQLite. ```rust Database::connect("sqlite:file:mydb?mode=memory&cache=shared").await?; ``` **SQLite timestamp with time zone reads** (#2878) Timestamp-with-time-zone values preserve their stored offset instead of normalizing through UTC on read. **Tracing instrumentation no longer records SQL arguments by default** (#3047) Automatic `tracing::instrument` fields drop SQL arguments; SQL recording stays controlled by the explicit tracing span setting. ### Compatibility Notes - Serde shape for first-party wrappers is now explicit: `TextUuid` ↔ JSON string, Unix-timestamp wrappers ↔ JSON integers. - SQLite timestamp-with-time-zone reads preserve offsets; PostgreSQL `drop_everything` is intentionally more destructive for custom-type dependents. - The entity loader change adds methods to the public loader traits — generated code updates automatically, but manual trait impls must add them. - `default-features = false` users of streaming must enable `stream`. - Tracing spans no longer carry SQL statements/arguments as automatic `instrument` fields; re-enable via the explicit tracing span setting if needed. - Nested prefixes are opt-in; existing unprefixed `#[sea_orm(nested)]` fields still compile. Reusing the same non-empty prefix for the same nested type is a compile error (the fields would decode from identical aliases).