changelog/2.0.0-rc.41.md
(since 2.0.0-rc.40)
SelectFourMany with consolidate() (#3054)
SelectFour::consolidate() returns a SelectFourMany; its all() (star topology)
rolls the three has-many children up into Vecs — Vec<(E::Model, Vec<F::Model>, Vec<G::Model>, Vec<H::Model>)> — mirroring SelectThreeMany instead of returning
flat Option tuples.
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.
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:
#[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.
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.
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 i64s matching their stored value.
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.
# 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.
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:
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.
#[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:
#[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:
--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.
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.
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.
TextUuid ↔ JSON string,
Unix-timestamp wrappers ↔ JSON integers.drop_everything is intentionally more destructive for custom-type dependents.default-features = false users of streaming must enable stream.instrument fields; re-enable via the explicit tracing span setting if needed.#[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).