Releases: SeaQL/sea-orm
2.0.0-rc.38
Release Notes: SeaORM 2.0.0-rc.38
(since 2.0.0-rc.37)
New Features
find_both_related() β required inner join loader (#2997)
A new find_both_related() method returns Vec<(E::Model, F::Model)> (both sides non-optional), as a counterpart to the existing find_also_related() which returns Vec<(E::Model, Option<F::Model>)>. Use this when you know the relation is always populated and want to avoid the Option unwrap:
let results: Vec<(Order, LineItem)> = Order::find()
.find_both_related(LineItem)
.all(db)
.await?;set_ne / set_ne_and aliases on ActiveValue (#3040)
Shorter aliases for set_if_not_equals and set_if_not_equals_and. The long-form names are kept as compatibility aliases:
// Before
active_model.name.set_if_not_equals("Alice");
// After
active_model.name.set_ne("Alice");map_sqlx_*_pool_opts on ConnectOptions (#2770)
Three new methods to customise the underlying sqlx::pool::PoolOptions before the connection pool is created β one per driver:
ConnectOptions::new(DATABASE_URL)
.map_sqlx_postgres_pool_opts(|opts| opts.max_connections(20).min_connections(5))
.to_owned()BTreeMap / HashMap support in TryGetableFromJson (#3009)
Map types can now be used directly as model fields when the column holds a JSON object:
pub struct Model {
pub id: i32,
pub metadata: HashMap<String, serde_json::Value>,
}Inherited visibility in derive macros (#3029)
DeriveEntityModel, DeriveActiveModel, DeriveModel, and related macros now inherit the pub(crate) / pub(super) / private visibility of the struct they are applied to, instead of always emitting pub items.
Bug Fixes
Schema sync: drop unique constraint on PostgreSQL (#2994)
DROP INDEX fails on PostgreSQL for unique indexes created via column-level UNIQUE because they are backed by a named constraint, not a standalone index. Schema sync now uses ALTER TABLE β¦ DROP CONSTRAINT on PostgreSQL and falls back to DROP INDEX on other backends.
Schema sync: tables in non-default schemas now discovered (#3016)
sync() previously ran schema discovery only against CURRENT_SCHEMA(). Entities with #[sea_orm(schema_name = "other")] were never found, causing every sync to attempt a redundant CREATE TABLE. Discovery now collects all schemas referenced by registered entities and queries each one.
Nested PartialModel null detection for optional fields (#3039)
A nested Option<PartialModel> loaded via a left join could incorrectly fail with Missing value for column 'id' when the nested model itself contained Option<T> fields. The null check now correctly handles arbitrary Option nesting depth.
use_transaction per-migration config was ignored (#3002)
exec_with_connection unconditionally wrapped all PostgreSQL migration operations in a transaction, overriding the per-migration use_transaction setting. The macro has been removed and each call site now uses the correct connection type.
Proc macros failed on long type paths with newlines (#3031)
DeriveActiveModelEx and other derive macros used .replace(' ', "") to strip whitespace, which missed newlines in formatted type paths longer than ~50 characters. Replaced with .split_whitespace().collect().
TIMESTAMPTZ conversion in the Postgres proxy driver (#3004)
from_sqlx_postgres_row_to_proxy_row now correctly converts TIMESTAMPTZ columns, fixing a panic when using the proxy driver with timestamptz fields.
Dependency Updates
1.1.20
2.0.0-rc.37
New Features
ER Diagram Generation (sea-orm-cli generate entity --er-diagram)
sea-orm-cli can now generate a Mermaid ER diagram alongside the entity files. Pass --er-diagram to write entities.mermaid into the output directory:
sea-orm-cli generate entity -u postgres://... -o src/entity --er-diagramThe diagram annotates columns with PK, FK, and UK markers and renders all relations β including many-to-many via junction tables β as Mermaid erDiagram syntax. Example output:
PostgreSQL Statement Timeout (ConnectOptions::statement_timeout)
ConnectOptions now accepts a statement_timeout for PostgreSQL connections. The timeout is set via the connection options at connect time (no extra round-trip) and causes the server to abort any statement that exceeds the duration:
ConnectOptions::new(DATABASE_URL)
.statement_timeout(Duration::from_secs(30))
.to_owned()Has no effect on MySQL or SQLite connections.
SQLite ?mode= URL Parameter Support (#2987)
The rusqlite driver now parses the ?mode= query parameter from SQLite connection URLs, matching the behaviour of the sqlx SQLite driver:
| Mode | Behaviour |
|---|---|
rwc (default) |
Read-write, create if not exists |
rw |
Read-write, must exist |
ro |
Read-only |
memory |
In-memory database |
// Open an existing database read-only
let db = Database::connect("sqlite:./data.db?mode=ro").await?;Unsupported parameters or unknown mode values return a DbErr::Conn error.
Bug Fixes
no-default-features compile errors with mac_address and proxy (#2992)
with-mac_addressfeature: added missingTryGetableimpls,try_from_u64impl, postgres array support, andwith-jsonserde flagproxyfeature: removed an accidental hard dependency onserde_json(now only activated viawith-json)- Fixed
cfgguards on JSON/JSONB proxy row handling to requirewith-json
2.0.0-rc.36
New Features
Per-Migration Transaction Control (#2980)
Previously, all Postgres migrations ran inside a single batch transaction, while MySQL and SQLite ran without one. This was an all-or-nothing approach with no way to opt out for individual migrations (e.g. CREATE INDEX CONCURRENTLY on Postgres requires running outside a transaction).
MigrationTrait now has a use_transaction() method to control this per migration:
impl MigrationTrait for Migration {
fn use_transaction(&self) -> Option<bool> {
Some(false) // opt out of automatic transaction
}
}None(default): follow backend convention β Postgres uses a transaction, MySQL/SQLite do notSome(true): force a transaction on any backendSome(false): disable automatic transaction wrapping
For migrations that opt out, SchemaManager::begin() and SchemaManager::commit() allow manual transaction control within the migration body:
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// DDL in a transaction
let m = manager.begin().await?;
m.create_table(
Table::create()
.table("my_table")
.col(pk_auto("id"))
.col(string("name"))
.to_owned(),
).await?;
m.commit().await?;
// Non-transactional DDL
manager.get_connection()
.execute_unprepared("CREATE INDEX CONCURRENTLY idx_name ON my_table (name)")
.await?;
Ok(())
}Core changes:
- Added
OwnedTransactionvariant toDatabaseExecutor, enablingSchemaManagerto own a transaction - Added
DatabaseExecutor::is_transaction()for runtime introspection - Each migration is now wrapped individually rather than in a batch
2.0.0-rc.35
New Features
SQLite Transaction Modes (#2932)
Added begin_with_options to TransactionTrait, allowing you to specify SQLite transaction modes (DEFERRED, IMMEDIATE, EXCLUSIVE), along with isolation level and access mode for other backends. Works for both sqlx-sqlite and rusqlite.
use sea_orm::{TransactionTrait, TransactionOptions, SqliteTransactionMode};
let txn = db.begin_with_options(TransactionOptions {
sqlite_transaction_mode: Some(SqliteTransactionMode::Immediate),
..Default::default()
}).await?;Nested transactions correctly fall back to SAVEPOINT regardless of the mode.
Extend DeriveIntoActiveModel (#2961)
DeriveIntoActiveModel now supports set, default, ignore, exhaustive, and custom active_model path attributes for fine-grained control when converting "form" or "input" structs into ActiveModels.
set β always set fields not present on the struct:
#[derive(DeriveIntoActiveModel)]
#[sea_orm(active_model = "fruit::ActiveModel", fill(cake_id = "None"))]
struct NewFruit {
name: String,
// cake_id is not on the struct, but will always be Set(None)
}
NewFruit { name: "Apple".into() }.into_active_model()
// => ActiveModel { id: NotSet, name: Set("Apple"), cake_id: Set(None) }Multiple set entries can be combined or split across attributes:
#[derive(DeriveIntoActiveModel)]
#[sea_orm(
active_model = "fruit::ActiveModel",
set(name = "String::from(\"cherry\")", cake_id = "None")
)]
struct IdOnlyFruit {
id: i32,
}
IdOnlyFruit { id: 1 }.into_active_model()
// => ActiveModel { id: Set(1), name: Set("cherry"), cake_id: Set(None) }default β fallback value when an Option<T> field is None:
#[derive(DeriveIntoActiveModel)]
#[sea_orm(active_model = "fruit::ActiveModel")]
struct NewFruit {
#[sea_orm(default = "String::from(\"Unnamed\")")]
name: Option<String>,
}
NewFruit { name: Some("Apple".into()) }.into_active_model()
// => ActiveModel { id: NotSet, name: Set("Apple"), cake_id: NotSet }
NewFruit { name: None }.into_active_model()
// => ActiveModel { id: NotSet, name: Set("Unnamed"), cake_id: NotSet }Bare #[sea_orm(default)] (without a value) uses Default::default() as the fallback. This also works with custom enum types that implement Into<Option<T>>.
ignore β exclude struct fields from the ActiveModel:
#[derive(DeriveIntoActiveModel)]
#[sea_orm(active_model = "fruit::ActiveModel")]
struct NewFruit {
name: String,
cake_id: i32,
#[sea_orm(ignore)]
_extra: String, // not mapped to ActiveModel
}exhaustive β require all ActiveModel fields to be either on the struct or in set(...):
#[derive(DeriveIntoActiveModel)]
#[sea_orm(active_model = "fruit::ActiveModel", exhaustive, set(cake_id = "None"))]
struct FullFruit {
id: i32,
name: String,
// cake_id is covered by set(...), so all fields are accounted for
}Combining everything β set + default + ignore + exhaustive:
#[derive(DeriveIntoActiveModel)]
#[sea_orm(active_model = "fruit::ActiveModel", exhaustive, set(cake_id = "None"))]
struct NewFruit {
id: i32,
#[sea_orm(default = "String::from(\"Unnamed\")")]
name: Option<String>,
}
NewFruit { id: 1, name: Some("Apple".into()) }.into_active_model()
// => ActiveModel { id: Set(1), name: Set("Apple"), cake_id: Set(None) }
NewFruit { id: 2, name: None }.into_active_model()
// => ActiveModel { id: Set(2), name: Set("Unnamed"), cake_id: Set(None) }IntoSimpleExpr for FunctionCall (#2822)
FunctionCall now implements IntoSimpleExpr, so function calls can be used directly in select expressions and filters without wrapping in SimpleExpr.
Arrow: Support Decimal64 and Fixed-Size Binary (#2957)
- Decimal columns with precision <= 18 now map to Arrow
Decimal64(was alwaysDecimal128) - Precision 19-38 maps to
Decimal128, above 38 toDecimal256 - Added
FixedSizeBinary(N)support via#[sea_orm(arrow_byte_width = N)] - Added
BinaryArray/LargeBinaryArray/FixedSizeBinaryArraytoValue::Bytesconversion
Optional time crate for Migrations (#2865)
Migrations can now use the time crate instead of std::time::SystemTime for timestamps, enabling compilation to WASM targets. Activate with the with-time feature on sea-orm-migration.
OpenTelemetry SpanKind::Client (#2937)
The db_span! macro now emits otel.kind = "client", ensuring database spans are properly recognized as client spans by APM tools (Datadog, Jaeger, etc.).
Bug Fixes
Fix unique column in schema sync (#2971)
Columns marked with #[sea_orm(unique)] are now correctly handled by the schema sync/diff builder, generating proper unique constraints instead of being silently ignored.
Fix DeriveArrowSchema with split attributes (#2973)
Fixed a compilation error when #[sea_orm(...)] attributes were split across multiple lines on the same field (e.g. #[sea_orm(primary_key)] and #[sea_orm(auto_increment = false)] separately). The macro now properly consumes attributes it doesn't recognize.
Map internal error types properly
Internal errors from the schema builder are now mapped to the correct DbErr variants instead of being lost or mistyped.
Improvements
Pi Spigot Example
The sea-orm-sync pi spigot example has been refactored into a tutorial-style example with:
- OOP
PiSpigotstruct with state machine pattern (new/step/finalize/to_state/from_state) clapCLI with--digits,--checkpoint, and--dbflags- Comprehensive tests against 1000 known digits of pi, including three-phase checkpoint/resume
- Tutorial README demonstrating how to add SQLite persistence to any program
2.0.0-rc.34
- Don't create index if column is already unique (entity first workflow) (#2950)
- Derive clone for topologies (#2954)
- Added
try_from_u64toDeriveValueType(#2958)
// Test for try_from_u64 attribute with type alias
type UserId = i32;
#[derive(Clone, Debug, PartialEq, Eq, DeriveValueType)]
#[sea_orm(try_from_u64)]
pub struct MyUserId(pub UserId);- Arrow / Parquet support (#2957)
- Added
ArrowSchema,DeriveArrowSchema - Support decimal with different formats
- Support timestamp with different timezone / resolution
- Added parquet example
- Added
2.0.0-rc.32
- permit passing custom derives to Model or ModelEx (#2933)
#[sea_orm::model]
#[derive(TS, ...)]
// Apply attributes specifically to the generated Model struct
#[sea_orm(model_attrs(ts(rename = "Fruit")))]
// Apply attributes specifically to the generated ModelEx struct
#[sea_orm(model_ex_attrs(ts(rename = "FruitEx")))]
struct Model {
// ...
}The code above expands to:
// ...
#[derive(TS, ...)]
#[ts(rename = "Fruit")]
struct Model {
// ...
}
// ...
#[derive(TS, ...)]
#[ts(rename = "FruitEx")]
struct ModelEx {
// ...
}2.0.0-rc.29
- Add missing lifetime hint to
EntityName::table_name(#2907) - [sea-orm-cli] Fix codegen to not generate relations to filtered entities (#2913)
- [sea-orm-cli] Fix enum variants starting with digits (#2905)
- Add wrapper type for storing Uuids as TEXT (#2914)
- Optimize exists;
PaginatorTrait::existsis moved toSelectExt(#2909) - Add tracing spans for database operations (#2885)
- Fix derive enums without per-case rename (#2922)
- Fix
DeriveIntoActiveModelonOption<T>fields (#2926)
#[derive(DeriveIntoActiveModel)]
#[sea_orm(active_model = "<fruit::Entity as EntityTrait>::ActiveModel")]
struct PartialFruit {
cake_id: Option<i32>,
}
assert_eq!(
PartialFruit { cake_id: Some(1) }.into_active_model(),
fruit::ActiveModel {
id: NotSet,
name: NotSet,
cake_id: Set(Some(1)),
}
);FromQueryResultnow supports nullable nested model (#2845)
#[derive(FromQueryResult)]
struct CakeWithOptionalBakeryModel {
#[sea_orm(alias = "cake_id")]
id: i32,
#[sea_orm(alias = "cake_name")]
name: String,
#[sea_orm(nested)]
bakery: Option<bakery::Model>, // can be null
}