## 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<F::Model>,
Vec<G::Model>, Vec<H::Model>)>` — 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))?; // "\"<uuid>\""
serde_json::from_str::<TextUuid>("\"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<user::Entity>,
#[sea_orm(belongs_to, from = "recipient_id", to = "id")]
pub recipient: HasOne<user::Entity>, // 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).
