Skip to main content

61 posts tagged with "news"

View All Tags

SeaORM 2.0: Nested ActiveModel and Cascade Operations

Β· 17 min read
SeaQL Team
Chris Tsang
SeaORM 2.0 Banner

In our last post, we introduced a Smart Entity Loader that made querying multi-path relations into nested models simple and efficient. That solved the read side of the problem.

With nested ActiveModel, you can now do the reverse: persist a nested object back into the database in one operation. SeaORM walks the tree, detects changes, constructs the insert and update statements, and executes them in the correct order to respect foreign key dependencies.

Gist​

The following operation saves a new set of user + profile + post + tag + post_tag into the database atomically:

let user = user::ActiveModel::builder()
.set_name("Bob")
.set_email("bob@sea-ql.org")
.set_profile(profile::ActiveModel::builder().set_picture("image.jpg"))
.add_post(
post::ActiveModel::builder()
.set_title("Nice weather")
.add_tag(tag::ActiveModel::builder().set_tag("sunny")),
)
.save(db)
.await?;

Unfolding​

This builder pattern constructs the following object tree:

user::ActiveModelEx {
name: Set("Bob".into()),
email: Set("bob@sea-ql.org".into()),
profile: HasOneModel::Set(profile::ActiveModelEx {
picture: Set("image.jpg".into()),
..Default::default()
}),
posts: HasManyModel::Append(post::ActiveModelEx {
title: Set("Nice weather".into()),
tags: HasManyModel::Append(tag::ActiveModel {
tag: Set("sunny".into()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
}
.save(db)
.await?

.. which is equivalent to doing the following manually:

let txn = db.begin().await?;

// insert a user
let user = user::ActiveModelEx {
name: Set("Bob".into()),
email: Set("bob@sea-ql.org".into()),
..Default::default()
}.insert(&txn).await?;

// profile belongs_to user (1-1)
let profile = profile::ActiveModelEx {
user_id: Set(user.id),
picture: Set("image.jpg".into()),
..Default::default()
}.insert(&txn).await?;

// post belongs_to user (1-N)
let post = post::ActiveModelEx {
user_id: Set(user.id),
title: Set("Nice weather".into()),
..Default::default()
}.insert(&txn).await?;

// insert a tag
let tag = tag::ActiveModel {
tag: Set("sunny".into()),
..Default::default()
}
.insert(&txn)
.await?;

// associate tag to post
post_tag::ActiveModel {
post_id: Set(post.id),
tag_id: Set(tag.id),
}
.insert(&txn)
.await?;

txn.commit().await?;

Relational Dependency​

The core of the problem lies in figuring the foreign key relations between Entities and executing the queries in the correct order. SeaORM supports the following:

Has One / Belongs To​

User 1-1 Profile

user.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(has_one)]
pub profile: HasOne<super::profile::Entity>,
..
}

There is a unique key on user_id, making this relation one-to-one.

profile.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "profile")]
pub struct Model {
#[sea_orm(unique)]
pub user_id: i32,
#[sea_orm(belongs_to, from = "user_id", to = "id")]
pub user: HasOne<super::user::Entity>,
..
}

Since profile belongs to user, the user has to be inserted first to obtain it's id.

In SeaORM, it doesn't matter which way the model is nested, it will be executed in the correct order.

// also okay:
profile::ActiveModel::builder()
.set_user(
user::ActiveModel::builder()
.set_name("Alice")
.set_email("alice@rust-lang.org"),
)
.set_picture("image.jpg")
.save(db)
.await?;

Has Many​

User 1-N Post

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "post")]
pub struct Model {
pub user_id: i32,
#[sea_orm(belongs_to, from = "user_id", to = "id")]
pub author: HasOne<super::user::Entity>,
..
}

This is very similar to 1-1, with the nested model being a vector instead of option.

There are two possible actions: Replace and Append. The default action is append, which is non-destructive.

Let's say Bob has written a new blog post, there is no reason having to query all posts bob has written; we can simply do the following:

// query user, but no posts
let bob = user::Entity::load().filter_by_email("bob@sea-ql.org").one(db).await?.unwrap();

let mut bob.into_active_model();
bob.posts.push(post::ActiveModel::builder().set_title("Another weekend"));
bob.save(db).await?; // INSERT INTO post ..

However, sometimes we do want an empty vector to mean 'delete all', or we want to specify the exact set of children. Then we can use Replace.

bob.posts.replace_all([]); // delete all
bob.posts.replace_all([post_1]); // retain only this post

This will result in the following actions, where posts other than post 1 will be deleted:

SELECT FROM post WHERE user_id = bob.id
DELETE FROM post WHERE id = 2

Many to Many​

Post M-N Tag

Many-to-many relations are essential when modeling complex schemas. A unique feature of SeaORM is that it models many-to-many relations as a first-class construct, so you don't need to think about the junction table.

post.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "post")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub title: String,
#[sea_orm(has_many, via = "post_tag")] // β¬… specify junction table
pub tags: HasMany<super::tag::Entity>,
..
}
post_tag.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "post_tag")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)] // β¬… composite key
pub post_id: i32,
#[sea_orm(primary_key, auto_increment = false)] // β¬… composite key
pub tag_id: i32,
#[sea_orm(belongs_to, from = "post_id", to = "id")] // β¬… belongs to both
pub post: Option<super::post::Entity>,
#[sea_orm(belongs_to, from = "tag_id", to = "id")] // β¬… belongs to both
pub tag: Option<super::tag::Entity>,
}
tag.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "tag")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub tag: String,
}

M-N relations is not just 1-N + 1-1, it actually breaks away from the notion. Let's look at the following example, insert a new post with 2 tags:

// Insert one tag for later use
let sunny = tag::ActiveModel::builder().set_tag("sunny").save(db).await?;

// Insert a new post with 2 tags
let post = post::ActiveModel::builder()
.set_title("A sunny day")
.set_user(bob)
.add_tag(sunny) // an existing tag
.add_tag(tag::ActiveModel::builder().set_tag("outdoor")) // a new tag
.save(db) // new tag will be created and associcated to the new post
.await?;

Which results in the following actions:

INSERT INTO post (user_id, title) VALUES (bob.id, 'A sunny day') RETURNING id
INSERT INTO tag (tag) VALUES ('outdoor') RETURNING id
INSERT INTO post_tag (post_id, tag_id) VALUES (post.id, sunny.id) (post.id, outdoor.id)

Their relation is no longer "belongs to", they are just associated with each other. Removing tags from post does not delete the tags, but only the associations in the junction table.

post.tags.replace_all([outdoor]); // let's say we remove the tag sunny

Results in:

DELETE FROM post_tag WHERE (post_id, tag_id) IN ((post.id, sunny.id))

One more example to make this easier to grasp: in a Film M-N Actor relation, deleting a film does not delete its actors, since they can still appear in other films.

Note​

SeaORM also supports using a surrogate primary key on the junction table, though a composite primary key is recommended.

film_actor.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "film_actor")]
pub struct Model {
#[sea_orm(primary_key)] // β¬… normal primary key
pub id: i32,
#[sea_orm(unique_key = "film_actor")] // β¬… unique key
pub film_id: i32,
#[sea_orm(unique_key = "film_actor")] // β¬… unique key
pub actor_id: i32,
#[sea_orm(belongs_to, from = "film_id", to = "id")]
pub film: HasOne<super::film::Entity>,
#[sea_orm(belongs_to, from = "actor_id", to = "id")]
pub actor: HasOne<super::actor::Entity>,
}

Change Detection​

Let's go back to the basics, in SeaORM every Model is backed by an ActiveModel:

post.rs
pub struct ModelEx {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: i32,
pub title: String,
pub author: HasOne<super::user::Entity>,
pub tags: HasMany<super::tag::Entity>,
}

// generated by macro:
pub struct ActiveModelEx {
#[sea_orm(primary_key)]
pub id: ActiveValue<i32>,
pub user_id: ActiveValue<i32>,
pub title: ActiveValue<String>,
pub author: HasOneModel<super::user::Entity>,
pub tags: HasManyModel<super::tag::Entity>,
}

Each ActiveValue is a tri-state.

pub enum ActiveValue<V>
{
Set(V),
Unchanged(V),
NotSet,
}

This allows SeaORM to keep track of what's changed. When you first query a fresh Model from database, the default state is Unchanged. When you perform some change in code, the state will be changed to Set. So when you run save, only the changed columns are updated.

let post = post::Entity::find_by_id(22).one(db).await?.unwrap(); // Model
let mut post = post.into_active_model(); // ActiveModel
post.title = Set("The weather changed!");
post.save(db).await?; // UPDATE post SET title = '..' WHERE id = 22

This has two advantages: it avoids over-updating, reducing the amount of data sent over the wire. More importantly, multiple API endpoints can safely update different column sets without risk of race conditions and without relying on transactions or locking mechanisms.

This concept is extended to nested ActiveModels, allowing SeaORM to walk the nested object tree and determine which sub-tree has been changed and requires updating.

For example:

let mut bob: user::ActiveModel = ..;

// update post title
bob.posts[0].title = Set("Lorem ipsum dolor sit amet".into());
// update post comment
bob.posts[0].comments[0].comment = Set("nice post! I learnt a lot".into());
// add a new comment too
bob.posts[1].comments.push(
comment::ActiveModel::builder().set_comment("interesting!")
);
bob.save(db).await?;

Results in:

BEGIN TRANSACTION

UPDATE post SET title = '..' WHERE id = post.id
UPDATE comment SET comment = '..' WHERE id = comment.id
INSERT INTO comment (post_id, comment) VALUES (post.id. '..')

COMMIT

It's a lot to take in, but once you build a clear mental model of SeaORM's concepts and mechanisms, you'll find yourself far more productive!

You can find all the techniques described in this blog post in a single-file example application.

Cascade Delete​

If the relations are defined with ON DELETE CASCADE, this problem does not exist. However, SeaORM can also perform cascade deletes on the client side. It applies the same rules described above, but in reverse.

For example, Post belongs to User. All posts must be deleted before the user; otherwise, the SQL operation will fail.

let user_4 = user::Entity::find_by_id(4).one(db).await?.unwrap();

user_4.cascade_delete(db).await?; // equivalent to below
user_4.into_ex().delete(db).await?;
-- query the profile belonging to user
SELECT FROM profile INNER JOIN user ON user.id = profile.user_id WHERE user.id = 4 LIMIT 1
-- delete the profile, if exist
DELETE FROM profile WHERE profile.id = 2
-- query the posts belonging to user
SELECT FROM post INNER JOIN user ON user.id = post.user_id WHERE user.id = 4
-- query the comments belonging to post
SELECT FROM comment INNER JOIN post ON post.id = comment.post_id WHERE post.id = 7
-- delete the comments, if exist
DELETE FROM comment WHERE comment.id = 5
-- query the post's tags
SELECT FROM post_tag INNER JOIN post ON post_tag.post_id = post.id WHERE post.id = 7
-- delete the post-tag associations
DELETE FROM post_tag WHERE (post_id, tag_id) IN ((7, 2), (7, 3), (7, 4))
-- post has no dependents, safe to delete now
DELETE FROM post WHERE post.id = 7
-- user has no dependents, safe to delete now
DELETE FROM user WHERE user.id = 4

Weak Belongs To​

There is one more special case of the Belongs To relation not yet mentioned: weak 1-N associations, where an entity may have an owner, but is not strictly required.

To give an example, Post 1-N Attachment. However users can upload attachments before drafting posts, thus some attachments may have no associated posts.

attachment.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "attachment")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub post_id: Option<i32>, // this is nullable
pub file: String,
#[sea_orm(belongs_to, from = "post_id", to = "id")]
pub post: HasOne<super::post::Entity>,
}
// this post has attachment_2
let post_1 = post::Entity::find_by_id(1).one(db).await?.unwrap();
post_1.cascade_delete(db).await?;

// post_id of attachment will be set to null, instead of deleting the attachment
let attachment_2 = attachment::Entity::find_by_id(2).one(db).await?.unwrap();
assert!(attachment_2.post_id.is_none());

Idempotence​

The general rule of thumb is idempotence: saving an ActiveModel a second time should be a no-op. Unless you use replace or delete, no delete will be executed.

let post = post.save(db).await?;
let post = post.save(db).await?; // no-op, as all fields are unchanged

The ActiveModel you provides is a snapshot of the desired final state of the data, and SeaORM should ensure that it ends up that way. This can be complicated, so please report any bugs.

Tips​

Keeping track of whether to use insert or update can be hard, unless the intended action is "create new from scratch". Use save as the default, and let SeaORM decide when to execute insert or update.

(If you already find these concepts familiar, it's no surprise that it's called ActiveModel.)

Backwards Compatibility​

All the 2.0 features introduced in this post are fully backwards compatible with 1.0, since only new types and methods have been added: ActiveModelEx, HasOneModel, HasManyModel and a few methods. ActiveModel continues to behave exactly as before.

However, due to the macros needing the relations' attributes to generate the implementations, these features are only available to #[sea_orm::model], but not #[sea_orm::compact_model].

🧭 Instant GraphQL API​

With Seaography, the Entities you wrote can instantly be exposed as a GraphQL schema, with full CRUD, filtering and pagination. No extra macros, no Entity re-generation is needed!

With SeaORM and Seaography, you can prototype quickly and stay in the flow. The Entity:

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
#[sea_orm(unique)]
pub email: String,
#[sea_orm(has_one)]
pub profile: HasOne<super::profile::Entity>,
#[sea_orm(has_many)]
pub posts: HasMany<super::post::Entity>,
}

Instantly turned into a GraphQL type:

type User {
id: Int!
name: String!
email: String!
profile: Profile
post(
filters: PostFilterInput
orderBy: PostOrderInput
pagination: PaginationInput
): PostConnection!
}

πŸ–₯️ SeaORM Pro: Admin Panel​

SeaORM Pro is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have!

SeaORM Pro has been updated to support the latest features in SeaORM 2.0.

Features:

  • Full CRUD
  • Built on React + GraphQL
  • Built-in GraphQL resolver
  • Customize the UI with TOML config
  • Role Based Access Control (new in 2.0)

🌟 Sponsors​

Gold Sponsor​

QDX pioneers quantum dynamics-powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. We're grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications.

GitHub Sponsors​

If you feel generous, a small donation will be greatly appreciated, and goes a long way towards sustaining the organization.

A big shout out to our GitHub sponsors:

Godwin Effiong
Adam Israel
Ryan Swart
OteroRafael
Yuta Hinokuma
wh7f
MS
Numeus
Data Intuitive
Caido Community
Marcus Buffett
MasakiMiyazaki
KallyDev
Manfred Lee
Afonso Barracha
Dean Sheather

πŸ¦€ Rustacean Sticker Pack​

The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
  • Mascots: Ferris the Crab x 3, Terres the Hermit Crab
  • The Rustacean wordmark

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

What's new in SeaORM Pro 2.0

Β· 13 min read
SeaQL Team
Chris Tsang

SeaORM Pro is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required (but certainly nice to have).

An admin panel is essential for operating backend applications. But it often is an after-thought, or no dedicated resources is put into developing them.

SeaORM Pro is designed to bridge this gap, providing a solution that is both quick to implement and reliable for long-term use.

Table View​

There are two kinds of table view in SeaORM Pro, raw table and composite table:

Raw Table​

Each raw table corresponds to a table in the database, by default it will display all columns for all tables. You can configure the displayed columns and other settings via TOML.

pro_admin/raw_tables/product.toml
[table]
title = "Products"
table_size = "middle"
page_size = 30
order_by = { field = "product_id", order = "desc" }
columns = [
{ title = "ID", field = "product_id", width = 80 },
{ title = "Thumbnail", field = "thumb_nail_photo", input_type = "image", width = 120 },
{ title = "Product Category", field = "name", relation = "product_category", ellipsis = false, width = 180 },
]
hidden_columns = [ "size", "weight" ]
all_columns = false

Composite Table​

This is where SeaORM Pro shine. You can construct table views with data joining from multiple related tables. The underlying GraphQL query can be deeply nested.

Data from parent-child relations (e.g. Order -> OrderItem) are represented as collapsible nested tables. You can configure the settings via TOML.

pro_admin/composite_tables/sales_order.toml
[parent]
name = "sales_order_header"

[parent.table]
columns = [
{ title = "ID", field = "sales_order_id", width = 80 },
{ field = "order_date" },
{ field = "purchase_order_number" },
{ field = "account_number" },
{ field = "ship_method" },
{ field = "sub_total" },
{ field = "tax_amt" },
{ field = "freight" },
]
all_columns = false


[[children]]
relation = "customer"

[children.table]
columns = [
{ title = "ID", field = "customer_id", width = 80 },
{ field = "title", width = 100 },
{ field = "first_name", width = 120 },
{ field = "middle_name", width = 120 },
{ field = "last_name", width = 120 },
]
hidden_columns = [ "name_style", "suffix", "email_address", "phone", "rowguid", "created_date" ]


[[children]]
relation = "address1"

[children.table]
title = "Shipping Address"
columns = [
{ title = "ID", field = "address_id", width = 80 },
]
hidden_columns = [ "rowguid", "created_date" ]


[[children]]
relation = "address2"

[children.table]
title = "Billing Address"
columns = [
{ title = "ID", field = "address_id", width = 80 },
]
hidden_columns = [ "rowguid", "created_date" ]


[[children]]
relation = "sales_order_detail"

[children.table]
columns = [
{ title = "Thumbnail", field = "thumb_nail_photo", relation = "product", input_type = "image", width = 120 },
{ field = "name", relation = "product", width = 300 },
{ field = "product_number", relation = "product" },
{ field = "color", relation = "product" },
{ field = "size", relation = "product" },
{ field = "weight", relation = "product" },
{ field = "order_qty" },
{ field = "unit_price" },
{ field = "unit_price_discount" },
]
hidden_columns = [ "sales_order_id", "sales_order_detail_id", "product_id", "rowguid", "created_date" ]

Editor​

Data is not editable by default. You can configure create, update and delete forms via TOML. You can also configure which columns are editable. Non-editable columns will be shown as read-only.

Pop-up Editor​

By default, the pop-up editor will be used to create and update database table.

Create​

Enable create on this database table, this is disabled by default. Columns can be hidden from the create form but it's still visible on the view table.

pro_admin/raw_tables/product.toml
[create]
# Enable create for this table
enable = true
# Columns that are hidden on the create form
hidden_columns = [ "created_date" ]

Update​

Enable update on this database table, this is disabled by default. Columns can be hidden from the update form but it's still visible on the view table. Fields can also be readonly on the update form.

pro_admin/raw_tables/product.toml
[update]
# Enable update for this table
enable = true
# Columns that are hidden on the update form
hidden_columns = [ "created_date" ]
# Columns that are readonly on the update form
readonly_columns = [ "product_id" ]

Delete​

Enable delete on this database table, this is disabled by default.

pro_admin/raw_tables/product.toml
[delete]
# Enable delete for this table
enable = true

Model Editor​

Enable the use of model editor on this database table, this is disabled by default. The configuration of each fields are specified here in sequence, displaying across table view, create and update editor.

[editor]
# Enable model editor for this table
enable = true
# Title field to be shown
title_field = "address_line1"
# Display following columns in sequence
fields = [
{ title = "ID", field = "address_id", span = 8 },
{ field = "rowguid", span = 8 },
{ field = "created_date", span = 8 },
{ field = "address_line1", span = 12, input_type = "textarea", rows = 4 },
{ field = "address_line2", span = 12, input_type = "textarea", rows = 4 },
{ field = "city", span = 6 },
{ field = "state_province", span = 6 },
{ field = "country_region", span = 6 },
{ field = "postal_code", span = 6 },
]

Role-Based Access Control​

SeaORM Pro has been updated to support the latest features in SeaORM 2.0. Role-Based Access Control (RBAC) is fully integrated into SeaORM Pro Plus. It offers a GUI editor to edit RBAC permissions and assign user roles. Without the corresponding select permission, users will not be able to see relevant tables in the GUI. Similarly, edit buttons will be hidden if user does not have update permission.

Opt-in RBAC​

Upon upgrading to SeaORM 2.0, you can opt-in RBAC by enabling the rbac feature flag in the Rust backend:

Cargo.toml
seaography = { version = "2.0", features = ["rbac"] }

And enabling RBAC in the SeaORM Pro admin panel:

pro_admin/config.toml
[site.rbac]
# Is RBAC enabled?
enable = true

Role Permissions​

View Role Hierarchy Diagram​

Role has a self-referencing relation, and they form a DAG (Directed Acyclic Graph). Most commonly they form a hierarchy tree that somewhat resembles an organization chart.

A simple tree example:

admin <- manager <- public
<- ...etc.

Update Role Permission​

Each role has their own set of permissions. On runtime, the engine will walk the role hierarchy and take the union of all permissions of the sub-graph.

The actions we can perform on resources. There are 4 basic permissions, select, insert, update and delete. You can define more for your application.

Role Hierarchy​

Each role has their own set of permissions. On runtime, the engine will walk the role hierarchy and take the union of all permissions of the sub-graph.

User Role​

User has a 1-1 relationship with role, meaning each user can only be assigned at most 1 role.

User Override​

The schema is a mirror of above: User <-> Permission <-> Resource, with an extra grant field, false means deny.

What's Next?​

There's really a lot we want to build, to make SeaORM Pro suit the needs of every project. Please consider being a sponsor and take part in shaping its future!

Here's what we have in mind:

  • Single Sign On: To be able to sign-in with Google Workspace or Microsoft Business email.
  • Audit Log: And so, we'd want to keep a record of users' action and being able to audit them.
  • Advanced Dashboard: We want to make it super easy to design graphs and charts for the Admin Dashboard.
  • Tasks: To be able to visualize and control scheduled tasks, and kick start once off tasks in ad-hoc way.
  • Data Export: Export data to various formats, including CSV, Excel, and DataFrame!

🌟 Sponsors​

Gold Sponsor​

QDX pioneers quantum dynamics-powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. We're grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications.

GitHub Sponsors​

If you feel generous, a small donation will be greatly appreciated, and goes a long way towards sustaining the organization.

A big shout out to our GitHub sponsors:

Godwin Effiong
Adam Israel
Ryan Swart
OteroRafael
Yuta Hinokuma
wh7f
MS
Numeus
Data Intuitive
Caido Community
Marcus Buffett
MasakiMiyazaki
KallyDev
Manfred Lee
Afonso Barracha
Dean Sheather

πŸ¦€ Rustacean Sticker Pack​

The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
  • Mascots: Ferris the Crab x 3, Terres the Hermit Crab
  • The Rustacean wordmark

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

SeaORM 2.0: Strongly-Typed Column

Β· 10 min read
SeaQL Team
Chris Tsang
SeaORM 2.0 Banner

In our last post, we introduced a new Entity format - designed to be more concise, more readable, and easy to write by hand.

We've also added a new COLUMN constant to make it more ergonomic, along with other enhancements.

Bye-bye CamelCase​

Previously, column names in queries had to be written in CamelCase. This was because the Column type was defined as an enum, it's simpler for the type system and faster to compile than generating a struct per column, but at the cost of losing column‑specific type information.

Our new design keeps compilation fast while restoring stronger type guarantees. As a bonus, it eliminates the need for CamelCase and even saves a keystroke.

// old
user::Entity::find().filter(user::Column::Name.contains("Bob"))

// new
user::Entity::find().filter(user::COLUMN.name.contains("Bob"))

// compile error: the trait `From<{integer}>` is not implemented for `String`
user::Entity::find().filter(user::COLUMN.name.like(2))

Under the hood, each Column value is wrapped in a byte-sized struct TypeAwareColumn. This wrapper is generic over Entity, so whether a table has 1 column or 100, the compile‑time cost stays roughly the same.

COLUMN Constant​

pub struct NumericColumn<E: EntityTrait>(pub E::Column);

impl<E: EntityTrait> NumericColumn<E> {
pub fn eq<V>(v: V) -> Expr { .. }
}
user.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
..
}

// expands into following:

pub struct TypedColumn {
pub id: NumericColumn<Entity>,
pub name: StringColumn<Entity>,
pub date_of_birth: DateLikeColumn<Entity>,
}

pub const COLUMN: TypedColumn = TypedColumn {
id: NumericColumn(Column::Id),
name: StringColumn(Column::Name),
date_of_birth: DateLikeColumn(Column::DateOfBirth),
};

impl Entity {
pub const COLUMN: TypedColumn = COLUMN;
}

Type-Aware Helper Methods​

Each column type wrapper exposes a set of methods that's relevant for the column's type. For example StringColumn::contains and ArrayColumn::contains are distinct methods that do the right thing!

Entity::COLUMN.name.contains("Bob") // WHERE "name" LIKE '%Bob%'

// tags is Vec<String>
Entity::COLUMN.tags.contains(vec!["awesome"]) // WHERE "tags" @> ARRAY ['awesome']

Right now there are a set of types: BoolColumn, NumericColumn, StringColumn, BytesColumn, JsonColumn, DateLikeColumn, TimeLikeColumn, DateTimeLikeColumn, UuidColumn, IpNetworkColumn, and more relevant methods can be added, feel free to make sugguestions.

Column as typed value​

One advantage of this design is that Columns are values: you can pass them into functions, combine with reflection, and build safe dynamic queries:

// returns an Expression fragment that can be used to build queries
fn filter_by_column(col: post::Column) -> Expr {
col.eq("attribute")
}

// get an integer from a model depends on runtime condition
fn get_value_from(model: &post::Model, col: post::Column) {
let value: i32 = model.get(col).unwrap();
// do something on value
}

Opt-in Only​

These new structs are generated only when using the new #[sea_orm::model] or #[sea_orm::compact_model] macros. This keeps the change fully backwards‑compatible, and you incur no cost if you don't use them.

More Entity Enhancements​

A big thanks to early-adopters who provided feedback to improve SeaORM 2.0.

The nested types for HasOne and HasMany have been changed from transparent type aliases to wrapper types. This makes it possible to distinguish between a relation that hasn’t been loaded and one that has loaded but yielded no models.

pub enum HasOne<E: EntityTrait> {
#[default]
Unloaded,
NotFound,
Loaded(Box<<E as EntityTrait>::ModelEx>),
}

pub enum HasMany<E: EntityTrait> {
#[default]
Unloaded,
Loaded(Vec<<E as EntityTrait>::ModelEx>),
}

We've added a range of methods to the wrapper type to make it feel as transparent as possible. The goal is to reduce friction while still preserving the benefits of a strong type system.

// len() / is_empty() methods
assert_eq!(users[0].posts.len(), 2);
assert!(!users[0].posts.is_empty());

// impl PartialEq
assert_eq!(users[0].posts, [post_1, post_2]);

// this creates HasOne::Loaded(Box<profile::ModelEx>)
profile: HasOne::loaded(profile::Model {
id: 1,
picture: "jpeg".into(),
..
})

Entity Loader Paginator​

Entity Loader now supports pagination. It has the same API as a regular Select:

let paginator = user::Entity::load()
.with(profile::Entity)
.order_by_asc(user::COLUMN.id)
.paginate(db, 10);

let users: Vec<user::ModelEx> = paginator.fetch().await?;

Added delete_by_key​

In addition to find_by_key, now the delete_by_key convenience method is also added:

user::Entity::delete_by_email("bob@spam.com").exec(db).await?

The delete_by_* methods now return DeleteOne instead of DeleteMany. It doesn't change normal exec usage, but would change return type of exec_with_returning to Option<Model>:

fn delete_by_id<T>(values: T) -> DeleteMany<Self>         // old

fn delete_by_id<T>(values: T) -> ValidatedDeleteOne<Self> // new

Self-Referencing Relations​

Let's say we have a staff table, where each staff has a manager that they report to:

staff.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "staff")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub reports_to_id: Option<i32>,
#[sea_orm(
self_ref,
relation_enum = "ReportsTo",
relation_reverse = "Manages",
from = "reports_to_id",
to = "id"
)]
pub reports_to: HasOne<Entity>,
#[sea_orm(self_ref, relation_enum = "Manages", relation_reverse = "ReportsTo")]
pub manages: HasMany<Entity>,
}

Entity Loader​

let staff = staff::Entity::load()
.with(staff::Relation::ReportsTo)
.with(staff::Relation::Manages)
.all(db)
.await?;

assert_eq!(staff[0].name, "Alan");
assert_eq!(staff[0].reports_to, None);
assert_eq!(staff[0].manages[0].name, "Ben");
assert_eq!(staff[0].manages[1].name, "Alice");

assert_eq!(staff[1].name, "Ben");
assert_eq!(staff[1].reports_to.as_ref().unwrap().name, "Alan");
assert!(staff[1].manages.is_empty());

assert_eq!(staff[2].name, "Alice");
assert_eq!(staff[2].reports_to.as_ref().unwrap().name, "Alan");
assert!(staff[2].manages.is_empty());

assert_eq!(staff[3].name, "Elle");
assert_eq!(staff[3].reports_to, None);
assert!(staff[3].manages.is_empty());

Model Loader​

let staff = staff::Entity::find().all(db).await?;

let reports_to = staff
.load_self(staff::Entity, staff::Relation::ReportsTo, db)
.await?;

assert_eq!(staff[0].name, "Alan");
assert_eq!(reports_to[0], None);

assert_eq!(staff[1].name, "Ben");
assert_eq!(reports_to[1].as_ref().unwrap().name, "Alan");

assert_eq!(staff[2].name, "Alice");
assert_eq!(reports_to[2].as_ref().unwrap().name, "Alan");

assert_eq!(staff[3].name, "Elle");
assert_eq!(reports_to[3], None);

It can work in reverse too.

let manages = staff
.load_self_many(staff::Entity, staff::Relation::Manages, db)
.await?;

assert_eq!(staff[0].name, "Alan");
assert_eq!(manages[0].len(), 2);
assert_eq!(manages[0][0].name, "Ben");
assert_eq!(manages[0][1].name, "Alice");

assert_eq!(staff[1].name, "Ben");
assert_eq!(manages[1].len(), 0);

assert_eq!(staff[2].name, "Alice");
assert_eq!(manages[2].len(), 0);

assert_eq!(staff[3].name, "Elle");
assert_eq!(manages[3].len(), 0);

Unix Timestamp Column Type​

Sometimes it may be desirable (or no choice but) to store a timestamp as i64 in database, but mapping it to a DateTimeUtc in application code.

We've created a new set of UnixTimestamp wrapper types that does this transparently:

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "access_log")]
pub struct Model {
.. // with `chrono` crate
pub ts: ChronoUnixTimestamp,
pub ms: ChronoUnixTimestampMillis,
.. // with `time` crate
pub ts: TimeUnixTimestamp,
pub ms: TimeUnixTimestampMillis,
}
let now = ChronoUtc::now();
let log = access_log::ActiveModel {
ts: Set(now.into()),
..Default::default()
}
.insert(db)
.await?;

assert_eq!(log.ts.timestamp(), now.timestamp());

Entity-First Workflow​

SchemaBuilder can now be used in migrations.

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();

db.get_schema_builder()
.register(user::Entity)
.apply(db) // or sync(db)
.await
}
}

🧭 Instant GraphQL API​

With Seaography, the Entities you wrote can instantly be exposed as a GraphQL schema, with full CRUD, filtering and pagination. No extra macros, no Entity re-generation is needed!

With SeaORM and Seaography, you can prototype quickly and stay in the flow. The Entity:

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
#[sea_orm(unique)]
pub email: String,
#[sea_orm(has_one)]
pub profile: HasOne<super::profile::Entity>,
#[sea_orm(has_many)]
pub posts: HasMany<super::post::Entity>,
}

Instantly turned into a GraphQL type:

type User {
id: Int!
name: String!
email: String!
profile: Profile
post(
filters: PostFilterInput
orderBy: PostOrderInput
pagination: PaginationInput
): PostConnection!
}

πŸ–₯️ SeaORM Pro: Admin Panel​

SeaORM Pro is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have!

SeaORM Pro has been updated to support the latest features in SeaORM 2.0.

Features:

  • Full CRUD
  • Built on React + GraphQL
  • Built-in GraphQL resolver
  • Customize the UI with TOML config
  • Role Based Access Control (new in 2.0)

🌟 Sponsors​

Gold Sponsor​

QDX pioneers quantum dynamics-powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. We're grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications.

GitHub Sponsors​

If you feel generous, a small donation will be greatly appreciated, and goes a long way towards sustaining the organization.

A big shout out to our GitHub sponsors:


πŸ¦€ Rustacean Sticker Pack​

The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
  • Mascots: Ferris the Crab x 3, Terres the Hermit Crab
  • The Rustacean wordmark

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

SeaORM 2.0: Entity First Workflow

Β· 8 min read
SeaQL Team
Chris Tsang
SeaORM 2.0 Banner

In our last post, we introduced a new Entity format - designed to be more concise, more readable, and easy to write by hand.

With this format, you can embrace an Entity‑first workflow: stay focused on your domain model without getting bogged down in database tables or migration scripts.

And the best part? Pair it with Seaography and you'll have a working GraphQL API instantly - meaning you can skip writing most of the backend logic code until much later in your project's lifecycle.

This combination keeps you in the flow, and lets you focus on what really matters: building apps.

What's Entity first?​

SeaORM used to adopt a schema‑first approach: meaning you design database tables and write migration scripts first, then generate entities from that schema.

Entity‑first flips the flow: you hand-write the entity files, and let SeaORM generates the tables and foreign keys for you.

All you have to do is to add the following to your main.rs right after creating the database connection:

let db = &Database::connect(db_url).await?;
// synchronizes database schema with entity definitions
db.get_schema_registry("my_crate::entity::*").sync(db).await?;

This requires two feature flags schema-sync and entity-registry, and we're going to explain what they do.

Unfolding​

Entity Registry​

The above function get_schema_registry unfolds into the following:

db.get_schema_builder()
.register(comment::Entity)
.register(post::Entity)
.register(profile::Entity)
.register(user::Entity)
.sync(db)
.await?;

You might be wondering: how can SeaORM recognize my entities when, at compile time, the SeaORM crate itself has no knowledge of them?

Rest assured, there's no source‑file scanning or other hacks involved - this is powered by the brilliant inventory crate. The inventory crate works by registering items (called plugins) into linker-collected sections.

At compile-time, each Entity module registers itself to the global inventory along with their module paths and some metadata. On runtime, SeaORM then filters the Entities you requested and construct a SchemaBuilder.

The EntityRegistry is completely optional and just adds extra convenience, it's perfectly fine for you to register Entities manually like above.

Resolving Entity Relations​

If you remember from the previous post, you'll notice that comment has a foreign key referencing post. Since SQLite doesn't allow adding foreign keys after the fact, the post table must be created before the comment table.

This is where SeaORM shines: it automatically builds a dependency graph from your entities and determines the correct topological order to create the tables, so you don't have to keep track of them in your head.

Schema Sync in Action​

The second feature, schema-sync, compares the in‑memory entity definitions with the live database schema, detects missing tables, columns, and keys, and creates them idempotently - no matter how many times you run sync, the schema converges to the same state.

Let's walk through the different scenarios:

Adding Table​

Let's say you added a new Entity under mod.rs

entity/mod.rs
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14

pub mod prelude;

pub mod post;
pub mod upvote; // β¬… new entity module
..

The next time you cargo run, you'll see the following:

CREATE TABLE "upvote" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, .. )

This will create the table along with any foreign keys.

Adding Columns​

entity/profile.rs
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "profile")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub picture: String,
pub date_of_birth: Option<DateTimeUtc>, // β¬… new column
..
}

impl ActiveModelBehavior for ActiveModel {}

The next time you cargo run, you'll see the following:

ALTER TABLE "profile" ADD COLUMN "date_of_birth" timestamp with time zone

How about adding a non-nullable column? You can set a default_value or default_expr:

#[sea_orm(default_value = 0)]
pub post_count: i32,

// this doesn't work in SQLite
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub updated_at: DateTimeUtc,

Rename Column​

If you only want to rename the field name in code, you can simply remap the column name:

pub struct Model {
..
#[sea_orm(column_name = "date_of_birth")]
pub dob: Option<DateTimeUtc>, // β¬… renamed for brevity
}

This doesn't involve any schema change.

If you want to actually rename the column, then you have to add a special attribute. Note that you can't simply change the field name, as this will be recognized as adding a new column.

pub struct Model {
..
#[sea_orm(renamed_from = "date_of_birth")] // β¬… special annotation
pub dob: Option<DateTimeUtc>,
}

The next time you cargo run, you'll see the following:

ALTER TABLE "profile" RENAME COLUMN "date_of_birth" TO "dob"

Nice, isn't it?

Add Foreign Key​

Let's create a new table with a foreign key:

entity/upvote.rs
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "upvote")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub post_id: i32,
#[sea_orm(belongs_to, from = "post_id", to = "id")]
pub post: HasOne<super::post::Entity>,
..
}

impl ActiveModelBehavior for ActiveModel {}

The next time you cargo run, you'll see the following:

CREATE TABLE "upvote" (
"post_id" integer NOT NULL PRIMARY KEY,
..
FOREIGN KEY ("post_id") REFERENCES "post" ("id")
)

If however, the post relation is added after the table has been created, then the foreign key couldn't be created for SQLite. Relational queries would still work, but functions completely client-side.

Add Unique Key​

Now, let's say we've forgotten to add a unique constraint on user name:

entity/user.rs
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)] // β¬… add unique key
pub name: String,
#[sea_orm(unique)]
pub email: String,
..
}

The next time you cargo run, you'll see the following:

CREATE UNIQUE INDEX "idx-user-name" ON "user" ("name")

As mentioned in the previous blog post, you'll also get a shorthand method generated on the Entity:

user::Entity::find_by_name("Bob")..

Remove Unique Key​

Well, you've changed your mind and want to remove the unique constraint on user name:

pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
// no annotation
pub name: String,
#[sea_orm(unique)]
pub email: String,
..
}

The next time you cargo run, you'll see the following:

DROP INDEX "idx-user-name"

Footnotes​

Note that in general schema sync would not attempt to do any destructive actions, so meaning no DROP on tables, columns and foreign keys. Dropping index is an exception here.

Every time the application starts, a full schema discovery is performed. This may not be desirable in production, so sync is gated behind a feature flag schema-sync that can be turned off based on build profile.

🧭 Instant GraphQL API​

With Seaography, the Entities you wrote can instantly be exposed as a GraphQL schema, with full CRUD, filtering and pagination. No extra macros, no Entity re-generation is needed!

With SeaORM and Seaography, you can prototype quickly and stay in the flow. And because Seaography is highly customizable, you can gradually shift resolver logic into your own implementation as the application evolves, and layer access control on top before the project goes to production.

The Entity:

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
#[sea_orm(unique)]
pub email: String,
#[sea_orm(has_one)]
pub profile: HasOne<super::profile::Entity>,
#[sea_orm(has_many)]
pub posts: HasMany<super::post::Entity>,
}

Instantly turned into a GraphQL type:

type User {
id: Int!
name: String!
email: String!
profile: Profile
post(
filters: PostFilterInput
orderBy: PostOrderInput
pagination: PaginationInput
): PostConnection!
}

πŸ–₯️ SeaORM Pro: Admin Panel​

SeaORM Pro is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have!

SeaORM Pro has been updated to support the latest features in SeaORM 2.0.

Features:

  • Full CRUD
  • Built on React + GraphQL
  • Built-in GraphQL resolver
  • Customize the UI with TOML config
  • Role Based Access Control (new in 2.0)

πŸ¦€ Rustacean Sticker Pack​

The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
  • Mascots: Ferris the Crab x 3, Terres the Hermit Crab
  • The Rustacean wordmark

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

SeaORM 2.0: New Entity Format

Β· 14 min read
SeaQL Team
Chris Tsang
SeaORM 2.0 Banner

SeaORM’s current entity format is explicit, but it can feel verbose, making it difficult to write by hand. In SeaORM 2.0, we’re introducing a more information‑dense entity format, along with new features that make relational queries easier and more powerful.

Gist​

Suppose now we're making a blogging platform. We're designing a simple schema starting from users.

user 1-1 profile
user 1-N post
post 1-N comment

Defining the user Entity as follows:

mod user {
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
#[sea_orm(unique)]
pub email: String,
#[sea_orm(has_one)]
pub profile: HasOne<super::profile::Entity>,
#[sea_orm(has_many)]
pub posts: HasMany<super::post::Entity>,
}

impl ActiveModelBehavior for ActiveModel {}
}

You will be able to query a user with their profile along with all their blog posts in one operation:

// join paths:
// user -> profile
// user -> post -> comment
let user: Option<user::ModelEx> = user::Entity::load()
.filter_by_id(12)
.with(profile::Entity)
.with((post::Entity, comment::Entity))
.one(db)
.await?;

// has the following shape:
assert_eq!(user.unwrap(), user::ModelEx {
id: 12,
name: "Bob".into(),
email: "bob@sea-ql.org".into(),
profile: HasOne::Loaded(profile::ModelEx {
..
}),
posts: HasMany::Loaded(vec![
post::ModelEx {
title: "Nice day for a walk",
comments: vec![comment::ModelEx { .. }, .. ],
},
post::ModelEx { .. },
]),
});

Full example can be found here.

The Schema​

The definition of the user Entity is already shown above, let's look at profile and post:

mod profile {
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "profile")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub picture: String,
#[sea_orm(unique)]
pub user_id: i32,
#[sea_orm(belongs_to, from = "user_id", to = "id")]
pub user: HasOne<super::user::Entity>,
}

impl ActiveModelBehavior for ActiveModel {}
}

profile has a foreign key user_id -> user.id relating back to the user table. By applying a unique constraint, we're effectively making the relation 1-1.

mod post {
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "post")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: i32,
pub title: String,
pub body: String,
#[sea_orm(belongs_to, from = "user_id", to = "id")]
pub author: HasOne<super::user::Entity>,
#[sea_orm(has_many)]
pub comments: HasMany<super::comment::Entity>,
}

impl ActiveModelBehavior for ActiveModel {}
}

Similarly, post has a foreign key to the user table, but without the unique constraint. The comment entity is very similar.

mod comment {
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "comment")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub comment: String,
pub post_id: i32,
#[sea_orm(belongs_to, from = "post_id", to = "id")]
pub post: HasOne<super::post::Entity>,
}

impl ActiveModelBehavior for ActiveModel {}
}

Under the hood​

It looks clean right? But what magic is going on under the hood? Let's break it down step by step, at the end it actally expands into the current entity format. Such that this new entity format is perfectly backwards-compatible.

A bit of history​

In the early days of SeaORM, language servers didn't have very strong macro expansion capabilities. As such, types generated by derive macros can't be seen and picked up by IDEs for auto-completion. That's why in that era crates would prefer to be macro-light, it's also the reason SeaORM (still) has an expanded entity format. The current compact entity format expands into the expanded entity format.

An existing SeaORM entity has three sections, Model, Relation and ActiveModel.

use sea_orm::entity::prelude::*;

pub struct Model { .. }

pub enum Relation { .. }

impl Related<> for Entity {}

impl ActiveModelBehavior for ActiveModel {}

We've hid the Relation / Related section into the Model itself.

For the user entity, a Relation enum is generated:

pub enum Relation {
#[sea_orm(has_one = "super::profile::Entity")]
Profile,
#[sea_orm(has_many = "super::post::Entity")]
Post,
}

In addition, some Related impls are also generated:

impl Related<super::profile::Entity> for Entity {
fn to() -> RelationDef {
Relation::Profile.def()
}
}

impl Related<super::post::Entity> for Entity {
fn to() -> RelationDef {
Relation::Post.def()
}
}

For the profile entity, it gets slightly more elaborate:

pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}

impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}

Generating the Model​

Note that the Model is now not 'plain-old', as it has some nested fields:

pub struct Model {
pub id: i32,
pub name: String,
pub email: String,
pub profile: HasOne<super::profile::Entity>, // <-
pub posts: HasMany<super::post::Entity>, // <-
}

We have to drop these fields to make the struct simple, such that we can still do the following:

let user = user::Model { id: 1, name: "Bob".into(), email: "bob@sea-ql.org" };

That's why as you may have noticed, we need an attribute macro:

#[sea_orm::model]

We call the compound Model with nested fields ModelEx. May be it can have a better name, but you should rarely need to name this type explicitly.

More relation types​

What makes SeaORM stand-apart is that we support many different kinds of relations in real-world complex schemas. Many users may never encounter them, but they're there when you need them.

Many to many relation with junction table​

SeaORM is unique in its ability to model many‑to‑many relations as first‑class constructs. Most APIs treat both 1-N and M-N synonymously, and you never need to manually specify the junction table when writing queries.

Let's say post M-N tag, all you need is to specify the junction with the via attribute:

mod post {
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "post")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(has_many, via = "post_tag")]
pub tags: HasMany<super::tag::Entity>,
}

impl ActiveModelBehavior for ActiveModel {}
}

This is the junction table:

mod post_tag {
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "post_tag")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub post_id: i32,
#[sea_orm(primary_key, auto_increment = false)]
pub tag_id: i32,
#[sea_orm(belongs_to, from = "post_id", to = "id")]
pub post: Option<super::post::Entity>,
#[sea_orm(belongs_to, from = "tag_id", to = "id")]
pub tag: Option<super::tag::Entity>,
}

impl ActiveModelBehavior for ActiveModel {}
}

To query posts with tags, you call the exact same method, without even mentioning the junction.

let posts = post::Entity::load().with(tag::Entity).all(db).await?;

assert_eq!(posts, vec![post::ModelEx {
tags: vec![ tag::ModelEx { .. }, .. ]
}]);

// or you can use the model loader API:

let posts: Vec<post::Model> = post::Entity::find().all(db).await?;
let tags: Vec<tag::Model> = posts.load_many(tag::Entity, db).await?;

Self-referencing relations​

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "staff")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub manager_id: i32,
#[sea_orm(
self_ref,
relation_enum = "Manager",
from = "manager_id",
to = "id"
)]
pub manager: HasOne<Entity>,
}

The generated enum will have a variant like:

pub enum Relation {
#[sea_orm(
belongs_to = "Entity",
from = "Column::ManagerId",
to = "Column::Id",
)]
Manager, // <- relation_enum
}

This aspect is not so different from 1.0. A Related impl will not be generated, but the relation can still be used in loader or join queries.

Composite foreign key​

You don't use it very often, but SeaORM actually supports it since 0.1. Some lines are omitted for brevity.

mod composite_a {
#[sea_orm::model]
#[sea_orm(table_name = "composite_a")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique_key = "pair")] // <- name this unique key
pub left_id: i32,
#[sea_orm(unique_key = "pair")]
pub right_id: i32,
#[sea_orm(has_one)]
pub b: Option<super::composite_b::Entity>,
}
}

mod composite_b {
#[sea_orm::model]
#[sea_orm(table_name = "composite_b")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub left_id: i32,
pub right_id: i32,
#[sea_orm(
belongs_to,
from = "(left_id, right_id)",
to = "(left_id, right_id)"
)]
pub a: Option<super::composite_a::Entity>,
}
}

Designing an ORM that can support all these scenario is hard with exploding complexities, but this is exactly what makes SeaORM powerful and sets it apart. Even if you don’t need these features today, choosing SeaORM helps ensure your application is future-proof.

New find/filter by unique key methods​

To improve ergonomics, SeaORM 2.0 now automatically generates type-safe shorthand methods for unique keys:

user::Entity::find_by_email("bob@sea-ql.org").one(db).await?

user::Entity::load().filter_by_email("bob@sea-ql.org").one(db).await?

It even works on composite unique keys!

// the name `pair` is user-defined above
composite_a::Entity::find_by_pair((1, 2)).one(db).await?

composite_a::Entity::load().filter_by_pair((2, 3)).one(db).await?

(Smart) Entity Loader​

We've spent a lot of engineering effort in designing the new Entity Loader. You can see it as magic, because it eliminates the N+1 problem even when doing nested queries while preventing over-fetching at the same time.

In the nested query we shown you in the beginning, 3 queries are executed:

SELECT FROM user JOIN profile WHERE id = $
SELECT FROM post JOIN user WHERE user_id IN (..)
SELECT FROM comment WHERE post_id IN (..)

For 1-1 relations, it does a join and select up to three tables together in a single query.

For 1-N or M-N relations, it uses the data loader. Note that, it's a single query even for M-N relation, as the junction table will be joined.

For nested queries, it uses the data loader. It consolidates the id of all the posts in the 2nd query and issue one query for the comments.

Backwards compatibility​

The new Entity format is perfectly backwards compatible as it gets expanded into the current compact format. However, the Entity Loader generates a bit of extra code under the hood, and it's not available for compact entities. We've introduced a transitional macro, in case you want to take advantage of the Entity Loader without migrating to the new format.

#[sea_orm::compact_model] // <- add this
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "post")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: i32,
pub body: String,
// #[sea_orm(belongs_to, from = "user_id", to = "id")] // <- not needed
pub author: HasOne<super::user::Entity>, // <- add these compound fields
}

// the rest of the Entity file is exactly the same as before

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { .. }

Now you can do:

post::Entity::load().with(super::user::Entity)..

The current compact entity format has more flexibility, as you can:

  1. add new Relation enum variants
  2. add on_condition on relations

Updated codegen​

sea-orm-cli has been updated to generate this new entity format, including with Seaography support. You can run the same entity generate command, but with an additional --entity-format dense flag. We may turn this on by default in the future.

sea-orm-cli generate entity --output-dir ./src/entity --entity-format dense

More to come​

SeaORM 2.0 is shaping up to be our most significant release yet - with a few breaking changes, plenty of enhancements, and a clear focus on developer experience. We'll dive into Entity-first workflow in the next post, so keep an eye out for the next update!

SeaORM 2.0 will launch alongside SeaQuery 1.0. If you make extensive use of SeaQuery, we recommend checking out our earlier blog post on SeaQuery 1.0 to get familiar with the changes.

SeaORM 2.0 has reached its release candidate phase. We'd love for you to try it out and help shape the final release by sharing your feedback.

SQL Server Support​

SQL Server for SeaORM offers the same SeaORM API for MSSQL. We ported all test cases and examples, complemented by MSSQL specific documentation. If you are building enterprise software, you can request commercial access. It is currently based on SeaORM 1.0, but we will offer free upgrade to existing users when SeaORM 2.0 is finalized.

πŸ–₯️ SeaORM Pro: Admin Panel​

SeaORM Pro is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have!

SeaORM Pro has been updated to support the latest features in SeaORM 2.0.

Features:

  • Full CRUD
  • Built on React + GraphQL
  • Built-in GraphQL resolver
  • Customize the UI with TOML config
  • Role Based Access Control (new in 2.0)

🌟 Sponsors​

Gold Sponsor​

QDX pioneers quantum dynamics-powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. We're grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications.

GitHub Sponsors​

If you feel generous, a small donation will be greatly appreciated, and goes a long way towards sustaining the organization.

A big shout out to our GitHub sponsors:


πŸ¦€ Rustacean Sticker Pack​

The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
  • Mascots: Ferris the Crab x 3, Terres the Hermit Crab
  • The Rustacean wordmark

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

Seaography 2.0: A Powerful and Extensible GraphQL Framework 🧭

Β· 12 min read
SeaQL Team
Chris Tsang
Seaography 2.0 Banner

GraphQL has become the preferred interface for product teams. Both frontend and backend developers benefit from its type-safety, contractual guarantees, and composability. Yet the real challenge lies on the backend: implementing relational resolvers that can traverse complex schemas is often difficult and time-consuming.

Yes, there are libraries that can spin up a GraphQL resolver quickly, but they often come with trade-offs: they're hard to customize, making it painful to add additional endpoints when your application grows beyond the basics.

The hardest challenge is customization. Real-world applications demand fine-grained permissions and context-dependent business logic. Seaography is designed to solve this exact problem, offering:

  • Automatic GraphQL resolver generation with data loader integration to solve the N+1 problem
  • Extensive customization options and the ability to add custom endpoints easily
  • Authorization: Role-Based Access Control (RBAC) and fine-grained control with hooks / guards

🧭 What is Seaography​

A video is worth a thousand words, so let's look at a quick demo.

In under a minute, we've done the following:

  1. Generate SeaORM entities from an existing sakila database (SQLite in demo)
  2. Generate a GraphQL web server around the entities (supports Axum, Actix, Poem)
  3. Launch it and run some queries with GraphQL playground

This is of course a speed run, but you can follow the same steps easily, and the generated framework is fully customizable.

What kinds of queries are supported?​

Filter, Ordering and Pagination​

{
film(
filters: {
title: { contains: "sea" } # β¬… like '%sea%'
and: [{ releaseYear: { gt: "2000" } }, { length: { gt: 120 } }]
# ⬆ composable attribute filters
}
orderBy: { filmId: ASC }
pagination: { page: { page: 0, limit: 10 } }
# ⬆ cursor based pagination is also supported:
# pagination: { cursor: { limit: 10, cursor: "Int[3]:100" } }
) {
nodes {
filmId
title
description
}
paginationInfo {
pages
current
}
}
}

Nested Relational Query​

The following query finds us all the documentaries starred by the actor "David" along with the stores having it in stock so that we can go rent it.

{
film(
# ⬇ filter by related entity
having: { # β¬… where exists (..) AND (..)
actor: { firstName: { eq: "David" } }
category: { name: { eq: "Documentary" } }
}
) {
nodes {
filmId
title
# ⬇ skipped the film_actor junction
actor {
nodes {
firstName
lastName
}
}
# ⬇ nested relational query
inventory {
nodes {
store {
address {
address
city {
city
}
}
}
}
}
}
}
}

There are two join paths in this query:

film -> film_actor -> actor
-> inventory -> store -> address -> city

A data loader is used for resolving the relations, such that it does not suffers from the N+1 problem.

Mutations: create, update, delete​

Full CRUD is supported, including CreateOne CreateBatch Update and Delete.

mutation {
# ⬇ operations will be executed inside a transaction
filmTextCreateBatch(
data: [
{ filmId: 1, title: "Foo bar", description: "Lorem ipsum dolor sit amet" }
{ filmId: 2, title: "Fizz buzz", description: "Consectetur adipiscing elit" }
]
) {
filmId
title
description
}
}

Custom Query​

The above is not something entirely new, as some features already exist in Seaography 1.0. The real game-changer is how you can implement custom endpoints and mix-and-match them with SeaORM entities. Let's dive into it!

Custom Query with pagination​

Seaography 2.0 introduced a set of macros to allow you to write custom query endpoints by reusing facilities in Seaography.

Let's say we have a Customer entity:

//! This is an entity from the sakila schema, generated by sea-orm-cli
use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "customer")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub customer_id: i32,
pub store_id: i32,
pub first_name: String,
pub last_name: String,
..
}

We want to create a custom endpoint, like the one Seaography already provides, but with an additional requirement: only return customers of the current store from which the user makes request from.

use seaography::{apply_pagination, Connection, CustomFields, PaginationInput};

pub struct Operations;

#[CustomFields]
impl Operations {
async fn customer_of_current_store(
ctx: &Context<'_>,
pagination: PaginationInput,
// ⬆ this input struct is provided by Seaography
) -> async_graphql::Result<Connection<customer::Entity>> {
// this output struct ⬆ is provided by Seaography
let db = ctx.data::<DatabaseConnection>()?;
// ⬆ this is a normal SeaORM db connection
let session = ctx.data::<Session>()?;
// ⬆ this session is inject by the HTTP handler
let query = customer::Entity::find()
// ⬆ this is the same old SeaORM API
.filter(customer::Column::StoreId.eq(session.store_id));
// ⬆ here we implement our custom logic
// note that here, we haven't execute the query yet ..
// instead, we pass it to Seaography to handle the rest!
let connection = apply_pagination(&CONTEXT, db, query, pagination).await?;
// now the query executes ⬆

Ok(connection)
}
}

This would expose the following query endpoint:

customer_of_current_store(
pagination: PaginationInput
): CustomerConnection!

Query it like the following:

{
customer_of_current_store(pagination: { page: { page: 0, limit: 10 } }) {
nodes {
storeId
customerId
firstName
lastName
email
}
paginationInfo {
pages
current
}
}
}

It's almost effortless, right? In just a few lines of code, we've added a new API endpoint that does a lot under the hood. But the heavylifting is done by Seaography + SeaORM.

How does it work?​

On a very high-level, how it all works:

  1. Seaography bridges SeaORM types with Async GraphQL, such that any SeaORM entity can be used as GraphQL output
  2. The schema meta of SeaORM entities are transformed into GraphQL schema on-the-fly on application startup

The lifecycle of a GraphQL request:

  1. Async GraphQL parses the HTTP request and construct a GraphQL request context
  2. Your http handler intercepts this request and adds in additional session context
  3. This GraphQL request is passed to Seaography
  4. Seaography parses the input types and then construct the Rust types
  5. Your async resolver is called, performing some business logic and returns result to Seaography
  6. Seaography transforms the output and return to Async GraphQL
  7. Async GraphQL does some final checks and serializes everything into a HTTP response

You may wonder, isn't the above kind of already possible by using Async GraphQL's derive macros, for example, by deriving SimpleObject on a SeaORM entity?

Actually this is how Seaography 0.1 worked in its initial release. However, the complex queries we shown you in the beginning is only achievable with a dynamic schema, but in Async GraphQL the static and dynamic schemas are completely different type systems - they can't inter-operate ... until now!

The difference is, the transformation between SeaORM Model <-> GraphQL Model happens dynamically, so there's not a ton of code generated beneath the surface.

Custom Mutation​

Let's continue on making custom mutation endpoints. Say now we want to create a transactional endpoint for staff members in store to handle customer rentals.

First we can design the data structures for the input form:

use sea_orm::entity::prelude::{DateTimeUtc};
use seaography::{async_graphql, CustomFields, CustomInputType};

#[derive(Clone, CustomInputType)]
pub struct RentalRequest {
pub customer: String,
pub film: String,
pub coupon: Option<Coupon>,
pub timestamp: DateTimeUtc,
}

#[derive(Clone, CustomInputType)]
pub struct Coupon {
pub code: String,
pub points: Option<Decimal>,
}

Then we can define the mutation endpoint. The business logic is:

  1. Look up the specifc customer and film
  2. Find if there is inventory in store. If not, return error
  3. Create a new rental record and remove the item from inventory
#[CustomFields]
impl Operations {
async fn rental_request(
ctx: &Context<'_>,
rental_request: RentalRequest,
// ⬆ our custom input struct
) -> async_graphql::Result<rental::Model> {
// ⬆ a normal SeaORM Model
let db = ctx.data::<DatabaseConnection>()?;
let session = ctx.data::<Session>()?;
let txn = db.begin().await?;
// ⬆ create a transaction to make operation atomic

let customer = Customer::find_by_name(rental_request.customer, &txn).await?;
let film = Film::find_by_title(rental_request.film, &txn).await?;
// ⬆ helper methods to find the corresponding customer and film

// ⬇ find if there is inventory in current store
let inventory = Inventory::find()
.filter(inventory::Column::FilmId.eq(film.id))
.filter(inventory::Column::StoreId.eq(session.store_id))
.one(&txn)
.unwrap_or(Error::NoInventory)?;
// ⬆ return error if no inventory

let rental = rental::ActiveModel {
rental_date: Set(rental_request.timestamp),
inventory_id: Set(inventory.id),
customer_id: Set(customer.id),
staff_id: Set(session.staff_id), // β¬… current staff
last_update: Set(Utc::now()),
..Default::default()
}.insert(&txn).await?;

inventory.delete(&txn).await?;
// ⬆ now remove it from inventory
txn.commit().await?;
// ⬇ return the newly created rental record
Ok(rental)
}
}

The Coupon object is used to demonstrate that nested objects are supported, and it will be reflected in the GraphQL schema. I will leave it as an exercise for you to fit in the logic for handling it.

Custom methods and unions​

The GraphQL type system is very expressive (so is Rust), and so I want to demonstrate two more advanced features:

#[derive(Clone, CustomInputType, CustomOutputType)]
pub struct Rectangle {
pub origin: Point,
pub size: Size,
}

#[CustomFields]
impl Rectangle {
pub async fn area(&self) -> async_graphql::Result<f64> {
// ⬆ this is an instance method
Ok(self.size.width * self.size.height)
}
}

#[derive(Clone, CustomInputType, CustomOutputType)]
pub struct Circle {
pub center: Point,
pub radius: f64,
}

#[CustomFields]
impl Circle {
pub async fn area(&self) -> async_graphql::Result<f64> {
Ok(std::f64::consts::PI * self.radius * self.radius)
}
}

#[derive(Clone, CustomInputType, CustomOutputType)]
pub enum Shape {
Rectangle(Rectangle),
Circle(Circle),
Triangle(Triangle),
}

After registering as complex_custom_outputs, they will appear like below in the GraphQL schema:

union Shape = Rectangle | Circle | Triangle

type Rectangle {
origin: Point!
size: Size!
area: Float! # β¬… as a 'computed property'
}

The area method will only be invoked when a query includes this field. Note that it is an async function, so you can even do database queries inside the function. For example, you can return a SimpleObject from a related model.

The union type definition allows you to use union types in input / output, a very natural construct in Rust.

Full example can be found here.

Lifecycle hooks​

In Seaography, all logic is centralized in the same process, and it allows you to inject arbitrary custom logic throughout the request lifecycle using hooks. You can even implement access control this way.

Fine‑grained Access Control​

Imagine you have a drawing app, and users can only access projects they own. You can implement the access control logic like the following:

struct AccessControlHook;

impl LifecycleHooksInterface for AccessControlHook {
fn entity_filter(
&self,
ctx: &ResolverContext,
entity: &str,
_action: OperationType, // β¬… Read, Create, Update, Delete
) -> Option<Condition> {
let session = ctx.data::<Session>()?;
// ⬆ extract user session
match entity {
"Project" => Some(
Condition::all()
.add(project::Column::OwnerId.eq(session.user_id))
// ⬆ add custom filter condition
),
_ => None,
}
}
}

By registering that into Seaography, this function will be called every time an Entity is being accessed:

lazy_static::lazy_static! {
static ref CONTEXT : BuilderContext = {
BuilderContext {
hooks: LifecycleHooks::new(AccessControlHook),
..Default::default()
}
};
}

Other hooks​

There are many useful hooks for type conversion, access guard, event notification, etc.

pub trait LifecycleHooksInterface: Send + Sync {
/// This happens before an Entity is accessed
fn entity_guard(
&self, ctx: &ResolverContext, entity: &str, action: OperationType
) -> GuardAction {
GuardAction::Allow
}

/// This happens after an Entity is mutated
async fn entity_watch(
&self, ctx: &ResolverContext, entity: &str, action: OperationType
) {}
}

πŸ–₯️ SeaORM Pro: A Seaography Showcase​

With SeaORM Pro, you can launch a ready-to-use admin panel in minutes. Built on Seaography, it demonstrates the seamless integration of the full technology stack - async Rust backend, React frontend, and GraphQL as the protocol.

SeaORM Pro has been updated to support the latest features in SeaORM 2.0, with RBAC support now available for preview in SeaORM Pro Plus.

Features:

  • Full CRUD
  • Built on React + GraphQL
  • Customize the UI with TOML config
  • GraphQL resolver using Seaography
  • Custom GraphQL endpoints (new in 2.0)
  • Role Based Access Control (new in 2.0)

Conclusion​

It took us a long time to get here, but this is our vision for application development in Rust: a framework that makes it effortless to get started, gives developers a ton of functionality out of the box, and still provides the power and flexibility to build complex applications.

We're heavily inspired by tools in the Python, Ruby and node.js ecosystem. You can draw some parallels between Seaography and FastAPI:

SeaographyFastAPI
GraphQL APIRest API
GraphQL schemaJSON Schema
GraphQL PlaygroundSwagger UI
Rust native typesPydantic
SeaORMSQLModel

In another sense, Seaography is like PostGraphile, offering instant GraphQL API for SQL databases:

SeaographyPostGraphile
MySQL, Postgres, SQLite, SQL Server*Postgres
Compiled schemaRuntime generated schema
SeaORM's RBACPostgres' RLS
Lifecycle hooks, builder contextPlugins

Sponsors​

This Seaography release has been made possible through the generous sponsorship of QDX and their close collaboration with SeaQL.org. QDX has built their data-driven applications with the Seaography + SeaORM stack, and we are deeply grateful for their contributions - both financial and technical - that helped bring this release to reality.

We welcome companies to collaborate with SeaQL.org to adopt and unlock the full potential of the Rust + SeaQL ecosystem, with our team providing expert technical consulting to support their software development.

Gold Sponsor​

QDX pioneers quantum dynamics–powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. We're grateful to QDX for sponsoring SeaQL.org.

Role Based Access Control in SeaORM 2.0

Β· 14 min read
SeaQL Team
Chris Tsang
SeaORM 2.0 Banner

SeaORM 2.0 introduces Role-Based Access Control (RBAC), bringing first-class authorization into your data layer. No more bolting on ad-hoc permission checks or scattering business rules across services - SeaORM lets you define roles and permission rules and enforce access policies directly inside the database connection. It's a powerful tool for building multi‑faceted applications that demand authorization.

Overview of SeaORM RBAC​

Here is a high level overview of the design and the requirements that shaped them:

  1. Table‑level access control

Different user groups can only read or modify certain tables, e.g. customers can only read invoices, but not modify them.

Design: RBAC engine is table‑scoped so permissions can be expressed directly in terms of CRUD on tables.

  1. Simplicity of user assignment

Each user should have a clear, unambiguous role to avoid confusion.

Design: one user = one role. This prevents complexity of multiple roles per user.

  1. Role hierarchy and inheritance

We want to create roles that inherit from multiple roles like A = B + C where A will have the union of permissions from B and C.

We want to avoid duplicating permission sets across roles. For example, a 'Manager' should automatically get all 'Employee' permissions, plus extras.

Design: Hierarchical roles with multiple inheritance.

  1. Granular, composable permissions

We need to allow fine‑grained control like 'read customers but not update them'. We want permission grant to be easy to reason about.

Design: Role can be assigned set of permissions (CRUD) on resources (tables). Permissions are additive, once granted, cannot be taken away (but can be overridden on a per user basis).

  1. Extensibility

We want to extend beyond tables (e.g. application specific actions, or even non‑DB resources).

Design: Engine is generic - resource + permission abstraction can be applied to more than just CRUD operations on SQL tables.

  1. Wildcard for convenience

Sometimes we need to grant superusers full access without enumerating every resource/permission.

Design: Opt‑in * wildcard for 'all permissions' or 'all resources.'

  1. Per‑user overrides

Occasionally, a single user needs an exception (e.g. a contractor who can only read one table, or a manager who should be denied one sensitive table).

Design: User‑level overrides to grant/deny permissions.

Why reinventing the wheel?​

Our first thought is to look into the possibility of integrating an existing open-source RBAC engine, but we developed our own in the end, because we want to integrate it tightly with SeaORM.

  1. Rules and permissions live in the same database as your app

By storing roles and permissions in the same database as your application data, you keep everything in one place. There's no separate DSL, no external policy files, and no risk of your access rules drifting out of sync with your schema. So you can query and update RBAC rules in migrations, just like other tables. Having a single source of truth simplifies development and deployment.

  1. The hard part isn't expressing rules, it's enforcing them

Most policy engines are great at describing rules in abstract terms, but the real challenge is: how do you actually enforce those rules against SQL queries? With an external library, we still need to analyze raw SQL statements ourselves and match that up with the rule definitions. By embedding RBAC directly into SeaORM, we can analyze all queries and enforce those rules.

  1. Lightweight and performant

Because the RBAC engine is part of SeaORM itself, it's lightweight and integrated - no extra runtime or external dependency. The runtime cost is also minimal, and most importantly, you don't pay for what you don't use. This feature can be turned off completely.

Concepts​

Let's take a look at the RBAC schema and go through the entities.

Entities​

User​

The user table is defined by your application. SeaORM doesn't manage that. However we currently require it to have an integer key.

Role​

Each role comes with a set of privileges. For example, 'admin', 'sales manager' and 'customer service'.

Permissions​

The actions we can perform on resources. There are 4 basic permissions, select, insert, update and delete. You can define more for your application.

Resources​

The resources being accessed. In our case they are database tables.

Relations​

User <-> Role​

As mentioned in the design above, User has a 1-1 relationship with role, meaning each user can only be assigned at most 1 role.

Role Hierarchy​

Role has a self-referencing relation, and they form a DAG (Directed Acyclic Graph). Most commonly they form a hierarchy tree that somewhat resembles an organization chart.

A simple tree example:

admin <- manager <- sales
<- warehouse

If we add the following to the graph, such that each role can have multiple super roles, then it becomes a DAG.

admin <- sourcing <- warehouse

Each role has their own set of permissions. On runtime, the engine will walk the role hierarchy and take the union of all permissions of the sub-graph.

Role <-> Permission <-> Resource​

Each role can have many such entries, and permission is set for each resource individually. For example: manager - update - order.

User override​

The schema is a mirror of above: User <-> Permission <-> Resource, with an extra grant field, false means deny.

Usage​

There are two stages: rules definiton when the RBAC rules are defined, and runtime authorization when these rules are enforced.

Rules Definition​

You can actually update the rules using the provided SeaORM entities, but we provide a set of utilities to make mutating RBAC rules easier.

These methods are idempotent and can be used in migrations.

Create RBAC tables​

sea_orm::rbac::schema::create_tables(db, Default::default()).await?;

Add resources & permissions​

let mut context = RbacContext::load(db).await?;

let tables = [
baker::Entity.table_name(),
bakery::Entity.table_name(),
cake::Entity.table_name(),
cakes_bakers::Entity.table_name(),
customer::Entity.table_name(),
lineitem::Entity.table_name(),
order::Entity.table_name(),
"*", // WILDCARD
];

context.add_tables(db, &tables).await?;
context.add_crud_permissions(db).await?;

Define roles​

First we create the roles.

context.add_roles(db, &["admin", "manager", "public"]).await?;

Then we can define the role hierarchy.

admin <- manager <- public
context
.add_role_hierarchy(
db,
&[
RbacAddRoleHierarchy {
super_role: "admin",
role: "manager",
},
RbacAddRoleHierarchy {
super_role: "manager",
role: "public",
},
],
)
.await?;

Add role permissions​

The permission and resource sets will be multiplied, i.e. Cartesian product taken.

// public can select everything, here wildcard is used
context.add_role_permissions(db, "public", &["select"], &["*"]).await?;
// manager can create / update cake and baker
context
.add_role_permissions(
db,
"manager",
&["insert", "update"],
&["cake", "baker", "cakes_bakers"],
)
.await?;
// admin can CRUD everything
context
.add_role_permissions(db, "admin", &["insert", "update", "delete"], &["*"])
.await?;

Assign user role​

context
.assign_user_role(db, &[
// (user_id, role)
(1, "admin"),
(2, "manager"),
(3, "public"),
])
.await?;

Runtime Authorization​

With these rules defined, we can now use them in our application.

Initialize RBAC engine​

By default, it expects the RBAC tables are in the same database schema as the current connection. They can also be fetched from another database connection.

let db: &DbConn;

db.load_rbac().await?;

The RBAC rules are cached in memory and shared among all database connections via RwLock, so they can be reloaded anytime.

Authenticate user​

This can be done by a web framework, where the user identity is extracted from a JWT token from HTTP requests. Here we assign them manually.

use sea_orm::rbac::RbacUserId;
let admin = RbacUserId(1);
let manager = RbacUserId(2);
let public = RbacUserId(3);

Create restricted connection​

This is the key step. Once a RestrictedConnection is created, it is bounded to the user for the lifetime of the object. It is cheap to create and destroy them, as they are just Arc inside.

The RestrictedConnection implements the standard DatabaseConnection API, so it can be used in place of a normal DbConn.

All queries made through SeaORM, including through Entity (ActiveModel) or lower level APIs (Insert) are audited. Only queries with matching permissions will be executed. DDL (i.e. ALTER) and raw SQL are not supported for now, so they will be rejected.

let db: RestrictedConnection = db.restricted_for(admin)?;

By writing functions only accepting RestrictedConnection, you can safeguard all operations within the scope of the function, as there is no way from a type system sense for it to degrade into normal DatabaseConnection. (In Rust we normally don't do singleton / global scope, so any operation having global side effects is very obvious.)

// admin can create bakery
operation(db.restricted_for(admin)?).await?;

fn operation(db: RestrictedConnection) -> Result<(), DbErr> {
let seaside_bakery = bakery::ActiveModel {
name: Set("SeaSide Bakery".to_owned()),
profit_margin: Set(10.2),
..Default::default()
};

let res = Bakery::insert(seaside_bakery).exec(&db).await?;
let bakery: Option<bakery::Model> =
Bakery::find_by_id(res.last_insert_id).one(&db).await?;

assert_eq!(bakery.unwrap().name, "SeaSide Bakery");
Ok(())
}
// manager can't create bakery
operation(db.restricted_for(manager)?).await?;

fn operation(db: RestrictedConnection) -> Result<(), DbErr> {
assert!(matches!(
Bakery::insert(bakery::ActiveModel::default())
.exec(db)
.await,
Err(DbErr::AccessDenied { .. })
));
Ok(())
}
// manager can create cake & baker
operation(db.restricted_for(manager)?).await?;

fn operation(db: RestrictedConnection) -> Result<(), DbErr> {
cake::Entity::insert(cake::ActiveModel {
name: Set("Cheesecake".to_owned()),
price: Set(2.into()),
bakery_id: Set(Some(1)),
gluten_free: Set(false),
..Default::default()
})
.exec(&db)
.await?;

// transaction is supported: using async closure
db.transaction::<_, _, DbErr>(|txn| {
Box::pin(async move {
cake::Entity::insert(cake::ActiveModel {
name: Set("Chocolate".to_owned()),
price: Set(3.into()),
bakery_id: Set(Some(1)),
gluten_free: Set(true),
..Default::default()
})
.exec(txn)
.await?;

Ok(())
})
})
.await?;

// transaction using the begin / commit API
let txn: RestrictedTransaction = db.begin().await?;

baker::Entity::insert(baker::ActiveModel {
name: Set("Master Baker".to_owned()),
contact_details: Set(Default::default()),
bakery_id: Set(Some(1)),
..Default::default()
})
.exec(&txn)
.await?;

txn.commit().await?;
Ok(())
}

That's it! I hope the information above can get you started.

Conclusion​

RBAC is a new feature in SeaORM 2.0. We'd love for you to try it out and help shape the final release by sharing your feedback.

The RBAC engine is a first class construct in SeaORM, implemented underneath the application layer but above the database layer.

We believe this is the most robust approach: if it's implemented on the web framework level, it's easy to forget permission checks or some code paths can accidentally escape. Plus it will work for your application, whether you're building REST, gRPC, or GraphQL servers.

In SeaORM, analysis is done on SeaQuery AST, so it's almost free - we already have the AST in memory. Thanks to SeaQuery's feature-rich API, you can construct any complex query, including CTEs!

Compared to using the database engine's native access control capabilities, SeaORM is much easier to setup, reason about, and develop with. Plus it is database generic, so you can use it with SQLite.

Everything, including the code and rules is defined in one place, so you have a single source of truth. We believe Rust and SeaQL ecosystem is the best way to build performant, scalable and robust applications!

We'll dive into GraphQL with Seaography in the next post, so keep an eye out for the next update!

SQL Server Support​

SQL Server for SeaORM offers the same SeaORM API for MSSQL. We ported all test cases and examples, complemented by MSSQL specific documentation. If you are building enterprise software, you can request commercial access. It is currently based on SeaORM 1.0, but we will offer free upgrade to existing users when SeaORM 2.0 is finalized, including RBAC support.

πŸ–₯️ SeaORM Pro: Professional Admin Panel​

SeaORM Pro is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have!

Features:

  • Full CRUD
  • Built on React + GraphQL
  • Built-in GraphQL resolver
  • Customize the UI with TOML config
  • Custom GraphQL endpoints (new in 2.0)
  • Role Based Access Control (new in 2.0)

SeaORM Pro will be updated to support the latest features in SeaORM 2.0, RBAC support is now available for preview in SeaORM Pro Plus.

RBAC-related features in SeaORM Pro Plus:

  • Permission editor GUI
  • Role hierarchy visualization
  • User role assignment
  • Add/remove user overrides

Sponsors​

Gold Sponsor​

QDX pioneers quantum dynamics–powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. We're grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications.

GitHub Sponsors​

If you feel generous, a small donation will be greatly appreciated, and goes a long way towards sustaining the organization.

A big shout out to our GitHub sponsors πŸ˜‡:


πŸ¦€ Rustacean Sticker Pack​

The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
  • Mascots: Ferris the Crab x 3, Terres the Hermit Crab
  • The Rustacean wordmark

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

SeaORM 2.0: A closer look

Β· 15 min read
SeaQL Team
Chris Tsang
SeaORM 2.0 Banner

In the previous blog post, we highlighted some of the new features in SeaORM 2.0. In this post, we're going to take a closer look to some of the changes under the hood.

Overhauled Entity::insert_many​

#2628 We've received many issue reports around the insert_many API. Previously, insert_many shares the same helper struct with insert_one, which led to an awkard API:

let res = Bakery::insert_many(std::iter::empty())
.on_empty_do_nothing() // <- you needed to add this,
// otherwise insert empty [] would lead to error
.exec(db)
.await;

assert!(matches!(res, Ok(TryInsertResult::Empty)));

After careful consideration, we made a number of changes in 2.0:

  1. removed APIs (e.g. Insert::add) that can panic
  2. new helper struct InsertMany, last_insert_id is now Option<Value>
  3. on empty iterator, None (for last_insert_id) or vec![] (when returning) is returned on execution
  4. TryInsert API is unchanged

i.e. now last_insert_id is Option<Value> for InsertMany:

struct InsertManyResult<A: ActiveModelTrait>
{
pub last_insert_id: Option<<PrimaryKey<A> as PrimaryKeyTrait>::ValueType>,
}

Which means the awkardness is removed:

let res = Entity::insert_many::<ActiveModel, _>([]).exec(db).await;

assert_eq!(res?.last_insert_id, None); // insert nothing return None

let res = Entity::insert_many([ActiveModel { id: Set(1) }, ActiveModel { id: Set(2) }])
.exec(db)
.await;

assert_eq!(res?.last_insert_id, Some(2)); // insert something return Some

Exec with returning now returns a Vec<Model>, so it feels intuitive:

assert!(
Entity::insert_many::<ActiveModel, _>([])
.exec_with_returning(db)
.await?
.is_empty() // no footgun, nice
);

assert_eq!(
Entity::insert_many([
ActiveModel {
id: NotSet,
value: Set("two".into()),
}
])
.exec_with_returning(db)
.await
.unwrap(),
[
Model {
id: 2,
value: "two".into(),
}
]
);

Same on conflict API as before:

let res = Entity::insert_many([ActiveModel { id: Set(3) }, ActiveModel { id: Set(4) }])
.on_conflict_do_nothing()
.exec(db)
.await;

assert!(matches!(conflict_insert, Ok(TryInsertResult::Conflicted)));

Overhauled ConnectionTrait API​

#2657 We overhauled the ConnectionTrait API. execute, query_one, query_all, stream now takes in SeaQuery statement instead of raw SQL statement.

So you don't have to access the backend to build the query yourself.

// old
let query: SelectStatement = Entity::find().filter(..).into_query();
let backend = self.db.get_database_backend();
let stmt = backend.build(&query);
let rows = self.db.query_all(stmt).await?;

// new
let query: SelectStatement = Entity::find().filter(..).into_query();
let rows = self.db.query_all(&query).await?;

A new set of methods execute_raw, query_one_raw, query_all_raw, stream_raw is added, so you can still do the following:

let backend = self.db.get_database_backend();
let stmt = backend.build(&query);

// new
let rows = self.db.query_all_raw(stmt).await?;

Better error handling in UpdateOne / DeleteOne​

#2726 UpdateOne and DeleteOne no longer implement QueryFilter and QueryTrait directly. Those implementations could expose an incomplete SQL query with an incomplete condition that touches too many records.

// bad: the following is basically update all
let query: UpdateStatement = Update::one(cake::ActiveModel::default()).into_query();

To generate the right condition, we must make sure that the primary key is set on the input ActiveModel by calling the validate() method:

Update::one(active_model)
+ .validate()? // checks the query; may yield PrimaryKeyNotSet error
.build(DbBackend::Postgres)

Potential compile errors​

If you need to access the generated SQL query, convert into ValidatedUpdateOne/ValidatedDeleteOne first.

error[E0599]: no method named `build` found for struct `query::update::UpdateOne` in the current scope
--> src/entity/column.rs:607:22
|
> | / Update::one(active_model)
> | | .build(DbBackend::Postgres)
| | -^^^^^ method not found in `UpdateOne<A>`
| |_____________________|
|

Added has_many_via for reverse has many relation​

Consider the following entities:

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "bakery")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub manager_id: i32,
pub cashier_id: i32,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::worker::Entity",
from = "Column::ManagerId",
to = "super::worker::Column::Id"
)]
Manager,
#[sea_orm(
belongs_to = "super::worker::Entity",
from = "Column::CashierId",
to = "super::worker::Column::Id"
)]
Cashier,
}
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worker")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
}

There exist two relations between them:

Bakery -> Worker (Manager)
-> Worker (Cashier)

It's now possible to define the inverse side of the relations in Worker:

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worker")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::bakery::Entity", via = "Relation::Manager")]
BakeryManager,
#[sea_orm(has_many = "super::bakery::Entity", via = "Relation::Cashier")]
BakeryCashier,
}

These relations can then be used in queries:

assert_eq!(
worker::Entity::find().join(
JoinType::LeftJoin,
worker::Relation::BakeryManager.def(),
)
.build(DbBackend::Sqlite)
.to_string(),
r#"SELECT "worker"."id", "worker"."name" FROM "worker"
LEFT JOIN "bakery" ON "worker"."id" = "bakery"."manager_id""#
);

Use of transaction with generic connections​

You can already use TransactionTrait as a generic parameter to define functions accepting any connection object that can initiate transactions.

In SeaORM 2.0, there are new database-connection-like objects: RestrictedConnection and RestrictedTransaction. They implement ConnectionTrait and TransactionTrait, and behaves just like normal DatabaseConnections except that they performs additional checks on queries.

Connection typeAssociated transaction type
DatabaseConnectionDatabaseTransaction
RestrictedConnectionRestrictedTransaction
// new connection type
pub struct RestrictedConnection {
conn: DatabaseConnection, // just a wrapper
user_id: UserId,
}

impl TransactionTrait for RestrictedConnection {
type Transaction = RestrictedTransaction; // added associated type
}

Meaning the following would continue to work:

// accepts any one of DatabaseConnection / DatabaseTransaction / RestrictedConnection / RestrictedTransaction.
// nested transactions will be spawned for transaction objects
async fn perform_actions<C: TransactionTrait>(
db: &C,
actions: &[Action],
) -> Result<(), DbErr> {
let txn = db.begin().await?;

for action in actions {
txn.execute(perform(action)).await?;
}

txn.commit().await
}

Removing panics from API​

SeaORM has a large API surface. We've already removed a great number of unwraps from the codebase in 1.0 release, but some panics due to "mis-use of API" can still happen.

Once again, we've tried to remove the remaining panics.

  • #2630 Added new error variant BackendNotSupported. Previously, it panics with e.g. "Database backend doesn't support RETURNING"
let result = cake::Entity::insert_many([])
.exec_with_returning_keys(db)
.await;

if db.support_returning() {
// Postgres and SQLite
assert_eq!(result.unwrap(), []);
} else {
// MySQL
assert!(matches!(result, Err(DbErr::BackendNotSupported { .. })));
}
  • #2627 Added new error variant PrimaryKeyNotSet. Previously, it panics with "PrimaryKey is not set"
assert!(matches!(
Update::one(cake::ActiveModel {
..Default::default()
})
.exec(&db)
.await,
Err(DbErr::PrimaryKeyNotSet { .. })
));
  • #2634 Remove panics in Schema::create_enum_from_active_enum
// method can now return None
fn create_enum_from_active_enum<A>(&self) -> Option<TypeCreateStatement>
  • #2628 Remove panickable APIs from insert
    /// Add a Model to `Insert`
///
/// # Panics
///
/// Panics if the rows have different column sets from what've previously
/// been cached in the query statement
- pub fn add<M>(mut self, m: M) -> Self
  • #2637 Remove panics in loader

Enhancements​

These are small touch‑ups, but added up they can make a big difference.

Added shorthand for Postgres = ANY​

Added ColumnTrait::eq_any as a shorthand for the = ANY operator. Postgres only.

// old: have to import sea-query
use sea_orm::sea_query::{Expr, extension::postgres::PgFunc};

cake::Entity::find()
.filter(
// have to qualify column manually
Expr::col((cake::Entity, cake::Column::Id)).eq(PgFunc::any(vec![4, 5]))
);

// new: just use sea-orm
assert_eq!(
cake::Entity::find()
.filter(cake::Column::Id.eq_any(vec![4, 5]))
.build(DbBackend::Postgres)
.to_string(),
r#"SELECT "cake"."id", "cake"."name" FROM "cake"
WHERE "cake"."id" = ANY(ARRAY [4,5])"#
);

Added big_pk_auto​

// old
pub fn pk_auto<T: IntoIden>(name: T) -> ColumnDef {
integer(name).auto_increment().primary_key().take()
}

// new: same as above but use big integer
pub fn big_pk_auto<T: IntoIden>(name: T) -> ColumnDef {
big_integer(name).auto_increment().primary_key().take()
}

Added chrono::Utc to entity prelude​

pub type ChronoUtc = chrono::Utc;

We can now rely on sea-orm's re-export:

// old: chrono has to be added in Cargo.toml
let ts: ChronoDateTimeUtc = chrono::Utc::now();
// new: use sea-orm's re-export
let ts: ChronoDateTimeUtc = ChronoUtc::now();

Breaking changes​

Use &'static str in identifiers​

#2667 Changed IdenStatic and EntityName definition. This change stemmed from the revamp of the Iden type system in SeaQuery, in which &'static str now has slightly less overhead.

trait IdenStatic {
fn as_str(&self) -> &'static str; // added static lifetime
}
trait EntityName {
fn table_name(&self) -> &'static str; // added static lifetime
}

QueryBuilder is no longer object safe​

Removed DbBackend::get_query_builder() because QueryBuilder is no longer object safe. This change improved query building performance by 5-10%.

impl DbBackend {
// This is removed
- fn get_query_builder(&self) -> Box<dyn QueryBuilder>;
}

Previously dyn SqlWriter is used everywhere.

fn prepare_table_create_statement(
&self,
create: &TableCreateStatement,
sql: &mut dyn SqlWriter,
);

Now, it's a generic method:

fn prepare_table_create_statement(
&self,
create: &TableCreateStatement,
sql: &mut impl SqlWriter, // note the impl
);

This change shouldn't impact most users because we have the following API:

pub trait StatementBuilder {
fn build(&self, db_backend: &DbBackend) -> Statement;
}

// implemented for SelectStatement, InsertStatement, UpdateStatement, DeleteStatement, etc

Changed Database Connection​

#2671 DatabaseConnection is changed from enum to struct. The original enum is moved into DatabaseConnection::inner. The new enum is named DatabaseConnectionType.

This allows DatabaseConnection to hold additional metadata.

// old
pub enum DatabaseConnection {
SqlxMySqlPoolConnection(crate::SqlxMySqlPoolConnection),
SqlxPostgresPoolConnection(crate::SqlxPostgresPoolConnection),
SqlxSqlitePoolConnection(crate::SqlxSqlitePoolConnection),
..
}

// new
pub struct DatabaseConnection {
pub inner: DatabaseConnectionType,
..
}

pub enum DatabaseConnectionType {
SqlxMySqlPoolConnection(crate::SqlxMySqlPoolConnection),
SqlxPostgresPoolConnection(crate::SqlxPostgresPoolConnection),
SqlxSqlitePoolConnection(crate::SqlxSqlitePoolConnection),
..
}

Removed Derive Custom Column​

#2667 Removed DeriveCustomColumn macro and default_as_str trait method. This was a legacy of the expanded entity format.

// This is no longer supported:
#[derive(Copy, Clone, Debug, EnumIter, DeriveCustomColumn)]
pub enum Column {
Id,
Name,
}

impl IdenStatic for Column {
fn as_str(&self) -> &str {
match self {
Self::Name => "my_name",
_ => self.default_as_str(),
}
}
}
// Do the following instead:
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
#[sea_orm(column_name = "my_name")]
Name,
}

Upgrades​

  • tokio is now used in place of async-std in sea-orm-cli and examples as async-std has been deprecated.
  • Returning is now enabled for SQLite by default. SQLite introduced returning in 3.35 which was released in 2021, it should be the default by now.
  • #2596 Upgraded Rust Edition to 2024
  • Upgraded strum to 0.27

SQL Server Support​

SQL Server for SeaORM offers the same SeaORM API for MSSQL. We ported all test cases and examples, complemented by MSSQL specific documentation. If you are building enterprise software, you can request commercial access. It is currently based on SeaORM 1.0, but we will offer free upgrade to existing users when SeaORM 2.0 is finalized.

πŸ–₯️ SeaORM Pro: Professional Admin Panel​

SeaORM Pro is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have!

SeaORM Pro will be updated to support the latest features in SeaORM 2.0.

Features:

  • Full CRUD
  • Built on React + GraphQL
  • Built-in GraphQL resolver
  • Customize the UI with TOML config
  • Custom GraphQL endpoints (new in 2.0)
  • Role Based Access Control (new in 2.0)

More to come​

SeaORM 2.0 is shaping up to be our most significant release yet - with a few breaking changes, plenty of enhancements, and a clear focus on developer experience. We'll dive into Role Based Access Control in the next post, so keep an eye out for the next update!

SeaORM 2.0 will launch alongside SeaQuery 1.0. If you make extensive use of SeaORM's underlying query builder, we recommend checking out our earlier blog post on SeaQuery 1.0 to get familiar with the changes.

SeaORM 2.0 has reached its release candidate phase. We'd love for you to try it out and help shape the final release by sharing your feedback.

Sponsors​

If you feel generous, a small donation will be greatly appreciated, and goes a long way towards sustaining the organization.

Gold Sponsor​

QDX pioneers quantum dynamics–powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. We're grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications.

GitHub Sponsors​

A big shout out to our GitHub sponsors πŸ˜‡:

Subscribe Pro

Variant9
Ryan Swart
OteroRafael
Yuta Hinokuma
wh7f
MS
Numeus
Data Intuitive
Caido Community
Marcus Buffett

MasakiMiyazaki
KallyDev
Manfred Lee
Afonso Barracha
Dean Sheather

πŸ¦€ Rustacean Sticker Pack​

The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
  • Mascots: Ferris the Crab x 3, Terres the Hermit Crab
  • The Rustacean wordmark

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

A Sneak Peek at SeaORM 2.0

Β· 13 min read
SeaQL Team
Chris Tsang
SeaORM 2.0 Banner

SeaORM 1.0 debuted on 2024-08-04. Over the past year, we've shipped 16 minor releases - staying true to our promise of delivering new features without compromising stability.

While building new features in 1.0, we often found ourselves bending over backwards to avoid breaking changes, which meant leaving in a few bits that aren't exactly elegant, intuitive, or frankly, "footgun".

To make SeaORM friendlier and more intuitive for newcomers (and a little kinder to seasoned users too), we've decided it's time for a 2.0 release - one that embraces necessary breaking changes to clean things up and set a stronger foundation for the future.

1.0 New Features​

If you haven't been following every update, here's a quick tour of some quality-of-life improvements you can start using right now. Otherwise, you can skip to the 2.0 section.

Nested Select​

This is the most requested feature by far, and we've implemented nested select in SeaORM. We've added nested alias and ActiveEnum support too.

use sea_orm::DerivePartialModel;

#[derive(DerivePartialModel)]
#[sea_orm(entity = "cake::Entity", from_query_result)]
struct CakeWithFruit {
id: i32,
name: String,
#[sea_orm(nested)]
fruit: Option<Fruit>,
}

#[derive(DerivePartialModel)]
#[sea_orm(entity = "fruit::Entity", from_query_result)]
struct Fruit {
id: i32,
name: String,
}

let cakes: Vec<CakeWithFruit> = cake::Entity::find()
.left_join(fruit::Entity)
.into_partial_model()
.all(db)
.await?;

PartialModel -> ActiveModel​

DerivePartialModel got another extension to derive IntoActiveModel additionally. Absent attributes will be filled with NotSet. This allows you to use partial models to perform insert / updates as well.

#[derive(DerivePartialModel)]
#[sea_orm(entity = "cake::Entity", into_active_model)]
struct PartialCake {
id: i32,
name: String,
}

let partial_cake = PartialCake {
id: 12,
name: "Lemon Drizzle".to_owned(),
};

// this is now possible:
assert_eq!(
cake::ActiveModel {
..partial_cake.into_active_model()
},
cake::ActiveModel {
id: Set(12),
name: Set("Lemon Drizzle".to_owned()),
..Default::default()
}
);

Insert active models with non-uniform column sets​

Insert many now allows active models to have different column sets. Previously, it'd panic when encountering this. Missing columns will be filled with NULL. This makes seeding data a seamless operation.

let apple = cake_filling::ActiveModel {
cake_id: ActiveValue::set(2),
filling_id: ActiveValue::NotSet,
};
let orange = cake_filling::ActiveModel {
cake_id: ActiveValue::NotSet,
filling_id: ActiveValue::set(3),
};
assert_eq!(
Insert::<cake_filling::ActiveModel>::new()
.add_many([apple, orange])
.build(DbBackend::Postgres)
.to_string(),
r#"INSERT INTO "cake_filling" ("cake_id", "filling_id") VALUES (2, NULL), (NULL, 3)"#,
);

Support Postgres PgVector & IpNetwork​

Under feature flag postgres-vector / with-ipnetwork.

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "demo_table")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub embedding: PgVector,
pub ipaddress: IpNetwork,
}

2.0 New Features​

These are small touch‑ups, but added up they can make a big difference.

Nested Select on any Model​

#2642 Wait... we've seen this before? No, there is a small detail here: now every Model can be used in nested select! This requires a small breaking change to basically derive PartialModelTrait on regular Models. And also notice the removed from_query_result.

use sea_orm::DerivePartialModel;

#[derive(DerivePartialModel)]
#[sea_orm(entity = "cake::Entity")] // <- from_query_result not needed
struct CakeWithFruit {
id: i32,
name: String,
#[sea_orm(nested)]
fruit: Option<fruit::Model>, // <- this is just a regular Model
}

let cakes: Vec<CakeWithFruit> = cake::Entity::find()
.left_join(fruit::Entity)
.into_partial_model()
.all(db)
.await?;

Wrapper type as primary key​

#2643 Wrapper type derived with DeriveValueType can now be used as primary key. Now you can embrace Rust's type system to make your code more robust!

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "my_value_type")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: MyInteger,
}

#[derive(Clone, Debug, PartialEq, Eq, DeriveValueType)]
pub struct MyInteger(pub i32);
// only for i8 | i16 | i32 | i64 | u8 | u16 | u32 | u64

Multi-part unique keys​

#2651 You can now define unique keys that span multiple columns in Entity.

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "lineitem")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique_key = "item")]
pub order_id: i32,
#[sea_orm(unique_key = "item")]
pub cake_id: i32,
}
let stmts = Schema::new(backend).create_index_from_entity(lineitem::Entity);

assert_eq!(
stmts[0],
Index::create()
.name("idx-lineitem-item")
.table(lineitem::Entity)
.col(lineitem::Column::OrderId)
.col(lineitem::Column::CakeId)
.unique()
.take()
);

assert_eq!(
backend.build(stmts[0]),
r#"CREATE UNIQUE INDEX "idx-lineitem-item" ON "lineitem" ("order_id", "cake_id")"#
);

Allow missing fields when using ActiveModel::from_json​

#2599 Improved utility of ActiveModel::from_json when dealing with inputs coming from REST APIs.

Consider the following Entity:

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "cake")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32, // <- not nullable
pub name: String,
}

Previously, the following would result in error missing field "id". The usual solution is to add #[serde(skip_deserializing)] to the Model.

assert!(
cake::ActiveModel::from_json(json!({
"name": "Apple Pie",
})).is_err();
);

Now, the above will just work. The ActiveModel will be partially filled:

assert_eq!(
cake::ActiveModel::from_json(json!({
"name": "Apple Pie",
}))
.unwrap(),
cake::ActiveModel {
id: NotSet,
name: Set("Apple Pie".to_owned()),
}
);

How does it work under the hood? It's actually quite interesting. This requires a small breaking to the trait bound of the method.

2.0 Exciting New Features​

We've planned some exciting new features for SeaORM too.

Ergonomic Raw SQL​

While already explained in detail in a previous blog post, we've integrated the raw_sql! macro nicely into SeaORM. It's like the format! macro but without the risk of SQL injection. It supports nested parameter interpolation, array and tuple expansion, and even repeating group!

It's not a ground-breaking new feature, as similar functions exist in other dynamic languages. But it does unlock exciting new ways to use SeaORM. After all, SeaORM isn't just an ORM; it's a flexible SQL toolkit you can tailour to your own programming style. Use it as a backend-agnostic SQLx wrapper, SeaQuery with built-in connection management, or a lightweight ORM with enchanted raw SQL. The choice is yours!

Find Model by raw SQL​

let item = Item { id: 1 };

let cake: Option<cake::Model> = cake::Entity::find()
.from_raw_sql(raw_sql!(
Postgres,
r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = {item.id}"#
))
.one(&db)
.await?;

Find custom Model by raw SQL​

#[derive(FromQueryResult)]
struct Cake {
name: String,
#[sea_orm(nested)]
bakery: Option<Bakery>,
}

#[derive(FromQueryResult)]
struct Bakery {
#[sea_orm(alias = "bakery_name")]
name: String,
}

let cake_ids = [2, 3, 4]; // expanded by the `..` operator

let cake: Option<Cake> = Cake::find_by_statement(raw_sql!(
Sqlite,
r#"SELECT "cake"."name", "bakery"."name" AS "bakery_name"
FROM "cake"
LEFT JOIN "bakery" ON "cake"."bakery_id" = "bakery"."id"
WHERE "cake"."id" IN ({..cake_ids})"#
))
.one(db)
.await?;

Paginate raw SQL query​

You can paginate SelectorRaw and fetch Model in batches.

let ids = vec![1, 2, 3, 4];

let mut cake_pages = cake::Entity::find()
.from_raw_sql(raw_sql!(
Postgres,
r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" IN ({..ids})"#
))
.paginate(db, 10);

while let Some(cakes) = cake_pages.fetch_and_next().await? {
// Do something on cakes: Vec<cake::Model>
}

Role Based Access Control​

#2683 We will cover this in detail in a future blog post, but here's a sneak peek.

SeaORM RBAC​

  1. A hierarchical RBAC engine that is table scoped
    • a user has 1 (and only 1) role
    • a role has a set of permissions on a set of resources
      • permissions here are CRUD operations and resources are tables
      • but the engine is generic so can be used for other things
    • roles have hierarchy, and can inherit permissions from multiple roles
    • there is a wildcard * (opt-in) to grant all permissions or resources
    • individual users can have rules override
  2. A set of Entities to load / store the access control rules to / from database
  3. A query auditor that dissect queries for necessary permissions (implemented in SeaQuery)
  4. Integration of RBAC into SeaORM in form of RestrictedConnection. It implements ConnectionTrait, behaves like a normal connection, but will audit all queries and perform permission check before execution, and reject them accordingly. All Entity operations except raw SQL are supported. Complex nested joins, INSERT INTO SELECT FROM, and even CTE queries are supported.
// set the rules
rbac.add_roles(&db_conn, &["admin", "manager", "public"]).await?;
rbac.add_role_permissions(&db_conn, "admin", &["create"], &["bakery"]).await?;

// load rules from database
db_conn.load_rbac().await?;

// admin can create bakery
let db: RestrictedConnection = db_conn.restricted_for(admin)?;
let seaside_bakery = bakery::ActiveModel {
name: Set("SeaSide Bakery".to_owned()),
..Default::default()
};
assert!(Bakery::insert(seaside_bakery).exec(&db).await.is_ok());

// manager cannot create bakery
let db: RestrictedConnection = db_conn.restricted_for(manager)?;
assert!(matches!(
Bakery::insert(bakery::ActiveModel::default())
.exec(&db)
.await,
Err(DbErr::AccessDenied { .. })
));

// transaction works too
let txn: RestrictedTransaction = db.begin().await?;

baker::Entity::insert(baker::ActiveModel {
name: Set("Master Baker".to_owned()),
bakery_id: Set(Some(1)),
..Default::default()
})
.exec(&txn)
.await?;

txn.commit().await?;

πŸ–₯️ SeaORM Pro: Professional Admin Panel​

SeaORM Pro is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have!

Features:

  • Full CRUD
  • Built on React + GraphQL
  • Built-in GraphQL resolver
  • Customize the UI with simple TOML
  • RBAC (coming soon with SeaORM 2.0)

More to come​

SeaORM 2.0 is shaping up to be our most significant release yet - with a few breaking changes, plenty of enhancements, and a clear focus on developer experience. We'll unpack everything in the posts to come, so keep an eye out for the next update!

SeaORM 2.0 will launch alongside SeaQuery 1.0. If you make extensive use of SeaORM's underlying query builder, we recommend checking out our earlier blog post on SeaQuery 1.0 to get familiar with the changes.

If you have suggestions on breaking changes, you are welcome to post them in the discussions.

Sponsors​

If you feel generous, a small donation will be greatly appreciated, and goes a long way towards sustaining the organization.

Gold Sponsor​

QDX pioneers quantum dynamics–powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. We're grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications.

GitHub Sponsors​

A big shout out to our GitHub sponsors πŸ˜‡:

Subscribe Pro

Variant9
Ryan Swart
OteroRafael
Yuta Hinokuma
wh7f
MS
Numeus
Data Intuitive
Caido Community
Marcus Buffett

MasakiMiyazaki
KallyDev
Manfred Lee
Afonso Barracha
Dean Sheather

πŸ¦€ Rustacean Sticker Pack​

The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
  • Mascots: Ferris the Crab x 3, Terres the Hermit Crab
  • The Rustacean wordmark

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

The road to SeaQuery 1.0

Β· 20 min read
SeaQL Team
Chris Tsang
SeaQuery 1.0 Banner

SeaQuery 0.1.0 was first released on 2020-12-16 - it's been a few years! Since then, there have been 32 releases, each introducing a set of new features. As with many software projects, the organic evolution driven by a diverse community of open source contributors has led to occasional inconsistencies across the codebase. It's a good problem to have, and a testament to our vibrant community. But now, it's time to stabilize SeaQuery and address some of these issues.

A very brief recap of important SeaQuery verisons:

versiondatenotes
0.1.02020-12-16initial release
0.16.02021-09-02SeaORM 0.1
0.30.02023-07-20SeaORM 0.12
0.31.02024-08-02SeaORM 1.0
0.32.02024-10-17SeaORM 1.1
0.32.72025-08-06latest version

Architectural changes​

There are a few architectural changes that can only be made by breaking the API, so let's go through them one by one:

Forbid unsafe code​

#930 #![forbid(unsafe_code)] has been added to all workspace crates, ensuring that SeaQuery no longer contains any unsafe code. While only one instance of unsafe was previously used, and has now been removed, this change reinforces our commitment to maintaining code quality.

Unified Expr and SimpleExpr as one type​

#890 Previously, a lot of operator methods (e.g. eq) were duplicated across Expr and SimpleExpr, but the list of methods was slightly different for each. Also, it wasn't clear when to use each of the two types. The type conversions were sometimes non-obvious. It complicated the type system and made writing generic code difficult.

In 0.32.0, almost a year ago, we added ExprTrait (#771) to standardize and share the list of methods, and to allow calling them on other "lower-level" types like so: 1_i32.cast_as("REAL"). At that time, we decided to keep the original inherent methods for compatibility, at the cost of ~1300 lines of code bloat.

Later, we looked into the Expr vs SimpleExpr distinction. It turned out that Expr was originally meant to be a "namespace" of static constructors for SimpleExpr, similar to Func vs FunctionCall. Unlike Func, which is a unit struct, Expr has data fields, which led to Exprs being passed around, making it hard for functions to accept / return "expression fragments".

In 1.0, SimpleExpr is "merged into" Expr, meaning that SimpleExpr is now just a type alias of Expr. Both names can be used interchangeably. A lot of redundant type conversions (.into()) and generic code (T: ExprTrait) can now be removed.

The resulting "merged" type has all methods from the two original types, except for the methods defined by ExprTrait. Those inherent methods have been removed and have saved us 1300 lines of code.

enum Expr { // the AST node enum
Column(ColumnRef),
Tuple(Vec<Expr>),
..
}

type SimpleExpr = Expr; // now: just an alias

impl Expr {
pub fn equals<C>(self, col: C) -> Self; // removed
}

trait ExprTrait: Sized {
fn equals<C>(self, col: C) -> Expr; // please use this
}

Potential compile errors​

If you implemented some trait for both of those types, two impls for one type will no longer compile and you'll need to delete one of the impls.

If you encounter the following error, please add use sea_query::ExprTrait in scope.

error[E0599]: no method named `like` found for enum `sea_query::Expr` in the current scope
|
| Expr::col((self.entity_name(), *self)).like(s)
|
| fn like<L>(self, like: L) -> Expr
| ---- the method is available for `sea_query::Expr` here
|
= help: items from traits can only be used if the trait is in scope
help: trait `ExprTrait` which provides `like` is implemented but not in scope; perhaps you want to import it
|
-> + use sea_query::ExprTrait;
error[E0308]: mismatched types
--> src/sqlite/discovery.rs:27:57
|
| .and_where(Expr::col(Alias::new("type")).eq("table"))
| -- ^^^^^^^ expected `&Expr`, found `&str`
| |
| arguments to this method are incorrect
|
= note: expected reference `&sea_query::Expr`
found reference `&'static str`

Revamped Iden type system.​

#909 Previously, DynIden is lazily rendered, i.e. the identifier is only constructed while serializing the AST. Now, it's an eagerly rendered string Cow<'static, str>, constructed while constructing the AST.

pub type DynIden = SeaRc<dyn Iden>;               // old
pub struct DynIden(pub(crate) Cow<'static, str>); // new

pub struct SeaRc<I>(pub(crate) RcOrArc<I>); // old
pub struct SeaRc; // new

The implications of this new design are:

  1. Type info is erased from Iden early
  2. SeaRc is no longer an alias to Rc / Arc. As such, Send / Sync is removed from the trait Iden
  - SeaRc::clone(&from_tbl)      // old
+ from_tbl.clone() // new

Potential compile errors​

The method signature of Iden::unquoted is changed. If you're implementing Iden manually, you can modify it like below:

error[E0050]: method `unquoted` has 2 parameters but the declaration in trait `types::Iden::unquoted` has 1
--> src/tests_cfg.rs:31:17
|
| fn unquoted(&self, s: &mut dyn std::fmt::Write) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected 1 parameter, found 2
|
::: src/types.rs:63:17
|
| fn unquoted(&self) -> &str;
| ----- trait requires 1 parameter
impl Iden for Glyph {
- fn unquoted(&self, s: &mut dyn fmt::Write) {
+ fn unquoted(&self) -> &str {
- write!(
- s,
- "{}",
match self {
Self::Table => "glyph",
Self::Id => "id",
Self::Tokens => "tokens",
}
- )
- .unwrap();
}
}

Alias::new is no longer needed​

#882 SeaQuery encourages you to define all column / table identifiers in one place and use them throughout the project. But there are places where an alias is needed once off. Now &'static str is an Iden, so it can be used in all places where Alias are needed. The Alias type remains for backwards compatibility, so existing code should still compile. This can reduce the verbosity of code, for example:

let query = Query::select()
.from(Character::Table)
- .expr_as(Func::count(Expr::col(Character::Id)), Alias::new("count"))
+ .expr_as(Func::count(Expr::col(Character::Id)), "count")
.to_owned();
  - Alias::new(format!("r{i}")).into_iden()
+ format!("r{i}").into_iden()

Unboxed Value variants​

#925 Most Value variants are now unboxed (except BigDecimal and Array). Previously the size is 24 bytes, now it's 32.

assert_eq!(std::mem::size_of::<Value>(), 32);

If you were constructing / pattern matching Value variants manually, Box::new can now be removed and pattern matching is simpler.

It also improved performance because memory allocation and indirection is removed in most cases.

Potential compile errors​

If you encounter the following error, simply remove the Box

error[E0308]: mismatched types
|
> | Value::String(Some(Box::new(string_value.to_string()))));
| ---- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `String`, found `Box<String>`
| |
| arguments to this enum variant are incorrect

non_exhaustive AST node enums​

#891 #[non_exhaustive] are added to all AST node enums. It allows us to add new features and extend the AST without breaking the API.

+ #[non_exhaustive]
enum Mode {
Creation,
Alter,
TableAlter,
}

Potential compile errors​

If you encounter the following error, please add a wildcard match _ => {..}

error[E0004]: non-exhaustive patterns: `&_` not covered
|
| match table_ref {
| ^^^^^^^^^ pattern `&_` not covered
|
note: `TableRef` defined here
|
| pub enum TableRef {
| ^^^^^^^^^^^^^^^^^
= note: the matched value is of type `&TableRef`
= note: `TableRef` is marked as non-exhaustive, so a wildcard `_` is necessary to match exhaustively
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
| TableRef::FunctionCall(_, tbl) => SeaRc::clone(tbl),
-> | &_ => todo!(),

Reworked TableRef and ColumnRef​

#927 Previously, the TableRef variants are a product of all valid combinations of Option<Database>, Option<Schema>, Table and Option<Alias>. It is excessive and makes pattern matching difficult.

Now they're collapsed into one. It makes constructing and pattern-matching TableRef / ColumnRef much easier.

// the following variants are collapsed into one:
enum TableRef {
Table(DynIden),
SchemaTable(DynIden, DynIden),
DatabaseSchemaTable(DynIden, DynIden, DynIden),
TableAlias(DynIden, DynIden),
SchemaTableAlias(DynIden, DynIden, DynIden),
DatabaseSchemaTableAlias(DynIden, DynIden, DynIden, DynIden),
..
}
// now it's just:
enum TableRef {
Table(TableName, Option<DynIden>), // optional Alias
..
}

pub struct DatabaseName(pub DynIden);
pub struct SchemaName(pub Option<DatabaseName>, pub DynIden);
/// A table name, potentially qualified as [database.][schema.]table
pub struct TableName(pub Option<SchemaName>, pub DynIden);

Similarly for ColumnRef:

// before
enum ColumnRef {
Column(DynIden),
TableColumn(DynIden, DynIden),
SchemaTableColumn(DynIden, DynIden, DynIden),
Asterisk,
TableAsterisk(DynIden),
}
// now
enum ColumnRef {
/// A column name, potentially qualified as [database.][schema.][table.]column
Column(ColumnName),
/// An `*` expression, potentially qualified as [database.][schema.][table.]*
Asterisk(Option<TableName>),
}

pub struct ColumnName(pub Option<TableName>, pub DynIden);

Potential compile errors​

TableRef

error[E0061]: this enum variant takes 2 arguments but 1 argument was supplied
--> src/entity/relation.rs:526:15
|
> | from_tbl: TableRef::Table("foo".into_iden()),
| ^^^^^^^^^^^^^^^-------------------
| ||
| |expected `TableName`, found `DynIden`
| argument #2 of type `Option<DynIden>` is missing

It's recommended to use the IntoTableRef trait to convert types instead of constructing AST manually.

use sea_orm::sea_query::IntoTableRef;

from_tbl: "foo".into_table_ref(),

ColumnRef

error[E0277]: the trait bound `fn(std::option::Option<TableName>) -> sea_query::ColumnRef {sea_query::ColumnRef::Asterisk}: IntoColumnRef` is not satisfied
--> src/executor/query.rs:1599:21
|
> | .column(ColumnRef::Asterisk)
| ------ ^^^^^^^^^^^^^^^^^^^ the trait `sea_query::Iden` is not implemented for fn item `fn(std::option::Option<TableName>) -> sea_query::ColumnRef {sea_query::ColumnRef::Asterisk}`
| |
| required by a bound introduced by this call

error[E0308]: mismatched types
--> src/executor/query.rs:1607:54
|
> | SimpleExpr::Column(ColumnRef::Column("id".into_iden()))
| ----------------- ^^^^^^^^^^^^^^^^ expected `ColumnName`, found `DynIden`
| |
| arguments to this enum variant are incorrect

In the former case Asterisk has an additional inner Option<TableName>, you can simply put None.

.column(ColumnRef::Asterisk(None))

In the latter case, &'static str can now be used in most methods that accepts ColumnRef.

Expr::column("id")

New Features​

Query Audit​

#908 In order to support Role Based Access Control (RBAC) in SeaORM, a given SQL query has to be analyzed to determine what permissions are needed to act on which resources.

It supports all the query types: SELECT, INSERT, UPDATE, DELETE and CTE. This requires the audit feature flag.

let query = Query::select()
.columns([Char::Character])
.from(Char::Table)
.left_join(
Font::Table,
Expr::col((Char::Table, Char::FontId)).equals((Font::Table, Font::Id)),
)
.inner_join(
Glyph::Table,
Expr::col((Char::Table, Char::Character)).equals((Glyph::Table, Glyph::Image)),
)
.take();

assert_eq!(
query.to_string(PostgresQueryBuilder),
r#"SELECT "character"
FROM "character"
LEFT JOIN "font" ON "character"."font_id" = "font"."id"
INNER JOIN "glyph" ON "character"."character" = "glyph"."image""#
);

assert_eq!(
query.audit()?.selected_tables(),
[
Char::Table.into_iden(),
Font::Table.into_iden(),
Glyph::Table.into_iden(),
]
);

Ergonomic raw SQL​

#952 This is already covered in a previous blog post. In case you've missed it, we've created a new raw_query! macro with neat features to make writing raw SQL queries more ergononmic.

let a = 1;
struct B { b: i32 }
let b = B { b: 2 };
let c = "A";
let d = vec![3, 4, 5];

let query = sea_query::raw_query!(
PostgresQueryBuilder,
r#"SELECT ("size_w" + {a}) * {b.b} FROM "glyph"
WHERE "image" LIKE {c} AND "id" IN ({..d})"#
);

assert_eq!(
query.sql,
r#"SELECT ("size_w" + $1) * $2 FROM "glyph"
WHERE "image" LIKE $3 AND "id" IN ($4, $5, $6)"#
);
assert_eq!(
query.values,
Values(vec![1.into(), 2.into(), "A".into(), 3.into(), 4.into(), 5.into()])
);

The snippet above demonstrated:

  1. named parameter: {a} injected
  2. nested parameter access: {b.b} inner access
  3. array expansion: {..d} expanded into three parameters

Breaking Changes​

Replaced SERIAL with GENERATED BY DEFAULT AS IDENTITY (Postgres)​

#918 SERIAL is deprecated in Postgres because identity column (GENERATED AS IDENTITY) is more modern and, for example, can avoid sequence number quirks.

let table = Table::create()
.table(Char::Table)
.col(ColumnDef::new(Char::Id).integer().not_null().auto_increment().primary_key())
.to_owned();

assert_eq!(
table.to_string(PostgresQueryBuilder),
[
r#"CREATE TABLE "character" ("#,
r#""id" integer GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY,"#,
r#")"#,
].join(" ")
);

If you need to support legacy systems you can still do:

let table = Table::create()
.table(Char::Table)
.col(ColumnDef::new(Char::Id).custom("serial").not_null().primary_key())
.to_owned();

assert_eq!(
table.to_string(PostgresQueryBuilder),
[
r#"CREATE TABLE "character" ("#,
r#""id" serial NOT NULL PRIMARY KEY"#,
r#")"#,
].join(" ")
);

Changed IntoXXX traits into Into<XXX>​

Changed IntoCondition (etc) traits to be defined as trait IntoCondition: Into<Condition>. A blanket impl is added. Now IntoCondition and Into<Condition> are completely interchangable, but you can still use .into_condition() for readability.

// before
trait IntoCondition {
fn into_condition(self) -> Condition;
}

// now
trait IntoCondition: Into<Condition> {
fn into_condition(self) -> Condition {
self.into()
}
}

impl<T> IntoCondition for T where T: Into<Condition> {}

If you have manually implemented Into* traits, it may cause conflicts. You should rewrite your impls as as impl From<..> for TableRef.

Full list of changed traits:

Performance Improvements​

We benchmarked the query-building process - and found out that the bulk of the overhead came from serializing queries into strings, not from the AST building. By optimizing the string handling part of the serialization process, we improved the query-building performance by up to 15%!

Replaced write! with write_str​

#947 This simple but not-so-obvious change by far contributed the biggest gain.

We won't go into the details here, as there are two tracking issues in rust-lang:

  • format_args! is slow rust/#76490
  • Tracking issue for improving std::fmt::Arguments and format_args!() rust/#99012
// before
write!(
sql,
"CONSTRAINT {}{}{} ",
self.quote().left(),
name,
self.quote().right()
);

// now
sql.write_str("CONSTRAINT ");
sql.write_char(self.quote().left());
sql.write_str(name);
sql.write_char(self.quote().right());
sql.write_str(" ");

Refactored Writer to avoid string allocation​

#945 Less strings is better!

// before: an intermediate string is allocated
let value: String = self.value_to_string(value);
write!(sql, "{value}");

// now: write to the buffer directly
self.write_value(sql, value);

fn write_value(&self, sql: &mut dyn Write, value: &Value);

Refactored Tokenizer to avoid string allocation​

#952 Note that the tokenizer is not part of the runtime query-building code path, but still worth mentioning.

// before
enum Token {
Quoted(String),
Unquoted(String),
Space(String),
Punctuation(String),
}

// now
enum Token<'a> {
Quoted(&'a str),
Unquoted(&'a str),
Space(&'a str),
Punctuation(&'a str),
}

Release Plan​

SeaQuery 1.0 is currently an rc release, and we plan to finalize it soon - meaning no more major breaking changes. If you feel adventurous or want to use some of the latest features, you can upgrade today. Please let us know the problems you faced, this will help us and the community. If you have ideas / feedback please join the discussion on GitHub!

As SeaORM is based on top of SeaQuery, the breaking changes above would impact SeaORM users as well. We tried to minimize the impact to SeaORM users that use SeaQuery lightly and most migrations can be done mechanically.

After SeaQuery 1.0, it will be the most exciting release - SeaORM 2.0!

Our New Team​

SeaQuery 1.0 wouldn't have happened without two contributors who joined us recently - Dmitrii Aleksandrov and Huliiiiii. They've made huge contributions that helped define this release, and we're super grateful for the effort and care they've poured into the project.


Chris Tsang
Maintainer
Dmitrii Aleksandrov
Maintainer
Huliiiiii
Contributor

Sponsors​

If you feel generous, a small donation will be greatly appreciated, and goes a long way towards sustaining the organization.

Gold Sponsor​

QDX pioneers quantum dynamics–powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. We're grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications.

GitHub Sponsors​

A big shout out to our GitHub sponsors πŸ˜‡:

Subscribe Pro

Variant9
vealth
AurLemon
Ryan Swart
OteroRafael
Yuta Hinokuma
wh7f
MS
Numeus
Data Intuitive
Caido Community
Marcus Buffett

MasakiMiyazaki
KallyDev
Manfred Lee
Afonso Barracha
Dean Sheather

πŸ¦€ Rustacean Sticker Pack​

The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
  • Mascots: Ferris the Crab x 3, Terres the Hermit Crab
  • The Rustacean wordmark

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

SeaQuery just made writing raw SQL more enjoyable

Β· 8 min read
SeaQL Team
Chris Tsang
SeaQuery 1.0 Banner

You enjoy writing raw SQL queries, SeaQuery just made it better!

We've created a new raw_query! macro with neat features to make writing raw SQL queries more ergononmic.

The biggest headache when crafting complex queries is parameter binding, whether you use ? or $N assigning parameters manually is laborious and error-prone.

SeaQuery's new raw_query! macro is designed to solve this exact problem.

Gist​

let a = 1;
struct B { b: i32 }
let b = B { b: 2 };
let c = "A";
let d = vec![3, 4, 5];

let query = sea_query::raw_query!(
PostgresQueryBuilder,
r#"SELECT ("size_w" + {a}) * {b.b} FROM "glyph"
WHERE "image" LIKE {c} AND "id" IN ({..d})"#
);

assert_eq!(
query.sql,
r#"SELECT ("size_w" + $1) * $2 FROM "glyph"
WHERE "image" LIKE $3 AND "id" IN ($4, $5, $6)"#
);
assert_eq!(
query.values,
Values(vec![1.into(), 2.into(), "A".into(), 3.into(), 4.into(), 5.into()])
);

There are several features packed into the snippet above!

Let's have a quick overview and we'll dive into the details:

  1. named parameter: {a} injected
  2. nested parameter access: {b.b} inner access
  3. array expansion: {..d} expanded into three parameters

There are two more features that will be showcased later:

  1. tuple expansion: {values.0:2}
  2. repeating group: {..(values.0),}

Motivation​

While SeaQuery has long offered a way to build dynamic queries solving basically the same problem, not every one liked SeaQuery's code structure.

I recently came across this SQLx issue, and was enticed to solve it.

I am heavily inspired by Rust's format! macro and other languages' template literals for string interpolation, and figured it could probably be achieved in Rust as well!

Challenges​

First, the above cannot be implemented with standard macro_rules because of caller hygiene, such that expanded code cannot access the variables in the surrounding scope. But proc macros do not have this limitation.

Second, if we want to expand an array, the number of elements cannot be known until runtime. So a compile-time approach couldn't work.

Third, we have to support all of Rust's primitive and container types as well as third party data types.

Dive in​

Let's take a look at what the above code expands into!

let a = 1;
struct B {
b: i32,
}
let b = B { b: 2 };
let c = "A";
let d = <[_]>::into_vec(#[rustc_box] ::alloc::boxed::Box::new([3, 4, 5]));

let query = {
use sea_query::raw_sql::*;
let mut builder = RawSqlQueryBuilder::new(sea_query::PostgresQueryBuilder);
builder
.push_fragment("SELECT (\"size_w\" + ")
.push_parameters(1)
.push_fragment(") * ")
.push_parameters(1)
.push_fragment(" FROM \"glyph\"\n WHERE \"image\" LIKE ")
.push_parameters(1)
.push_fragment(" AND \"id\" IN (")
.push_parameters((&d).len())
.push_fragment(")");
let sql = builder.finish();
let mut query = seaql::query(&sql);
query = query.bind(&a);
query = query.bind(&b.b);
query = query.bind(&c);
for v in (&d).iter() {
query = query.bind(v);
}
query
};

I created RawSqlQueryBuilder that is somewhat similar to SeaQuery's query building backend. It serializes the query string in a single pass.

The derive macro first tokenize and parse the raw SQL to identify the splice points, and then call a special method push_parameters to push a variable number of parameters. This can be numbered, i.e. for Postgres.

After finishing the SQL building part, it then proceeds to bind the parameters. This mechanism is designed around SQLx's query API.

If the variable has a spread .. operator, we'd loop over it and bind all items.

Other ideas​

Originally I wanted to implement auto-expansion, meaning if the parameter is a container type, we'd expand it automagically. But there are quite a few special cases - most importantly Vec<u8> is used as bytes and should be bound as a single parameter. Moreover, in Postgres you can use arrays directly, and so we can't decide whether to expand.

More goodies​

Tuple expansion​

There are two more features that'll make your life much easier.

let var = (1, "2".to_owned(), 3);

let query = sea_query::raw_query!(
PostgresQueryBuilder,
"SELECT {var.0}, {var.1}, {var.2}"
);

let new_query = sea_query::raw_query!(
PostgresQueryBuilder,
r#"SELECT {var.0:2}"#
);

assert_eq!(query, new_query);

We can already support accessing tuple members, why not offer a range operator?

The : token is chosen because it somewhat resembles the Python operator. [0:2] is un-natural because tuple members in Rust can only be accessed by .0. Feel free to offer your thoughts!

It's not possible to automatically expand a tuple like an array because its arity (the number of elements) is not known at the time the macro is expanded. If the tuple consists of elements with a uniform type, it can be made iterable like a vector by implementing the appropriate traits. However, that approach doesn't apply in the case above, where the tuple's structure is not uniform.

You can do inserts with this:

let values = (1, "2", 3);

let query = sea_query::raw_query!(
MysqlQueryBuilder,
"INSERT INTO `glyph` (`aspect`, `image`, `font_size`) VALUES ({values.0:2})"
);
assert_eq!(
query.sql,
"INSERT INTO `glyph` (`aspect`, `image`, `font_size`) VALUES (?, ?, ?)"
);
assert_eq!(query.values, Values(vec![1.into(), "2".into(), 3.into()]));

You may ask, then how do we insert multiple elements? Which brings us to the next feature:

Insert Many​

When working with Rust and SQL, a common pain point is figuring out how to insert multiple rows efficiently. Writing raw SQL INSERT statements for each row is verbose, and while Postgres supports UNNEST for bulk inserts, its columnar API feels awkward and error-prone.

SeaQuery solves this by letting you build multi-row insert statements easily. With the raw_query! macro, you can pass in a vector of structs or tuples and generate a single INSERT INTO .. VALUES (..) , (..) , (..) query. This keeps your code clean, while still allowing you to do bulk insertions for efficiency. This solution is database agnostic so you can use it for MySQL and SQLite as well.

With Tuples​

let values = vec![(1, "2", 3), (4, "5", 6)];

let query = sea_query::raw_query!(
PostgresQueryBuilder,
r#"INSERT INTO "glyph" ("aspect", "image", "font_size")
VALUES {..(values.0:2),}"#
);
assert_eq!(
query.sql,
r#"INSERT INTO "glyph" ("aspect", "image", "font_size")
VALUES ($1, $2, $3), ($4, $5, $6)"#
);

To achieve this we designed the "repeating group" syntax, inspired by regex. Please let me explain:

It's expanded upon the previous example, in which values.0:2 means tuple expansion. We want to repeat this tuple as a group, surrounded by parenthesis, so we wrap it with (). Then we apply the same spread operator .. to expand the vector of tuples. Finally, the trailing , means they should be connected with ,.

With structs​

struct Item {
a: i32,
b: String,
c: u16,
}

let values = vec![
Item { a: 1, b: "2".to_owned(), c: 3 },
Item { a: 4, b: "5".to_owned(), c: 6 },
];

let query = sea_query::raw_query!(
PostgresQueryBuilder,
r#"INSERT INTO "glyph" ("aspect", "image", "font_size")
VALUES {..(values.a, values.b, values.c),}"#
);

This is equivalent to the previous example, but uses named parameters.

SQLx Integration​

SeaQuery offers tight SQLx integration. So in practice you can do (requires sqlx-utils feature flag):

let mut sql;
let res = sea_query::sqlx::sqlite::query!(
sql = r#"INSERT INTO "character"
("uuid", "font_size", "character")
VALUES {..(values.0:2),}"#
).execute(pool).await?;

Note the salient sql variable. SQLx's Query object can only borrow the SQL as &str, and so someone has to own the String. I couldn't think of a better API, suggestions welcome.

It calls the underlying Query's bind method directly, so no extra copy is involved. This is the lowest possible overhead!

One final example:

let mut character = Character { id: 1, font_size: 0 };
character.font_size = 18;

let res = sea_query::sqlx::sqlite::query!(
sql = r#"UPDATE "character"
SET "font_size" = {character.font_size}
WHERE "id" = {character.id}"#
).execute(pool).await?;

Full example can be found here.

It almost feels like a mini ORM ... although SeaORM 🐚 is still highly recommended!

Lightweightness​

SeaQuery without default features only has a handful of dependencies. But if you want to keep dependencies to an absolute minimum, you can depends on sea-query-derive directly.

$ cargo tree --no-default-features -e normal,build
sea-query-derive v1.0.0-rc.11 (proc-macro)
β”œβ”€β”€ proc-macro2 v1.0.94
β”‚ └── unicode-ident v1.0.12
β”œβ”€β”€ quote v1.0.40
β”‚ └── proc-macro2 v1.0.94 (*)
└── syn v2.0.100
β”œβ”€β”€ proc-macro2 v1.0.94 (*)
β”œβ”€β”€ quote v1.0.40 (*)
└── unicode-ident v1.0.12

Then you can use it like:

let sql;
let res: Vec<_> = sea_query_derive::raw_sql!(
sqlx::sqlite::query_as,
sql = "SELECT {var.0:2}"
).fetch_all(pool).await?;

SeaQuery 1.0​

This is just one of many new features we've added while preparing SeaQuery 1.0. This is currently an rc release, but we plan to finalize it soon. If you have ideas please join the discussion.

πŸ¦€ Rustacean Sticker Pack​

The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
  • Mascots: Ferris the Crab x 3, Terres the Hermit Crab
  • The Rustacean wordmark

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

What's new in SeaORM 1.1.12

Β· 12 min read
SeaQL Team
Chris Tsang
SeaORM 1.0 Banner

This blog post summarizes the new features and enhancements introduced in SeaORM 1.1:

New Features​

Implement DeriveValueType for enum strings​

DeriveValueType now supports enum types. It offers a simpler alternative to DeriveActiveEnum for client-side enums backed by string database types.

#[derive(DeriveValueType)]
#[sea_orm(value_type = "String")]
pub enum Tag {
Hard,
Soft,
}

// `from_str` defaults to `std::str::FromStr::from_str`
impl std::str::FromStr for Tag {
type Err = sea_orm::sea_query::ValueTypeErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { .. }
}

// `to_str` defaults to `std::string::ToString::to_string`.
impl std::fmt::Display for Tag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { .. }
}

The following trait impl are generated, removing the boilerplate previously needed:

DeriveValueType expansion
#[automatically_derived]
impl std::convert::From<Tag> for sea_orm::Value {
fn from(source: Tag) -> Self {
std::string::ToString::to_string(&source).into()
}
}

#[automatically_derived]
impl sea_orm::TryGetable for Tag {
fn try_get_by<I: sea_orm::ColIdx>(res: &sea_orm::QueryResult, idx: I)
-> std::result::Result<Self, sea_orm::TryGetError> {
let string = String::try_get_by(res, idx)?;
std::str::FromStr::from_str(&string).map_err(|err| sea_orm::TryGetError::DbErr(sea_orm::DbErr::Type(format!("{err:?}"))))
}
}

#[automatically_derived]
impl sea_orm::sea_query::ValueType for Tag {
fn try_from(v: sea_orm::Value) -> std::result::Result<Self, sea_orm::sea_query::ValueTypeErr> {
let string = <String as sea_orm::sea_query::ValueType>::try_from(v)?;
std::str::FromStr::from_str(&string).map_err(|_| sea_orm::sea_query::ValueTypeErr)
}

fn type_name() -> std::string::String {
stringify!(Tag).to_owned()
}

fn array_type() -> sea_orm::sea_query::ArrayType {
sea_orm::sea_query::ArrayType::String
}

fn column_type() -> sea_orm::sea_query::ColumnType {
sea_orm::sea_query::ColumnType::String(sea_orm::sea_query::StringLen::None)
}
}

#[automatically_derived]
impl sea_orm::sea_query::Nullable for Tag {
fn null() -> sea_orm::Value {
sea_orm::Value::String(None)
}
}

You can override from_str and to_str with custom functions, which is especially useful if you're using strum::Display and strum::EnumString, or manually implemented methods:

#[derive(DeriveValueType)]
#[sea_orm(
value_type = "String",
from_str = "Tag::from_str",
to_str = "Tag::to_str"
)]
pub enum Tag {
Color,
Grey,
}

impl Tag {
fn from_str(s: &str) -> Result<Self, ValueTypeErr> { .. }

fn to_str(&self) -> &'static str { .. }
}

Support Postgres IpNetwork​

#2395 (under feature flag with-ipnetwork)

// Model
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "host_network")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub ipaddress: IpNetwork,
#[sea_orm(column_type = "Cidr")]
pub network: IpNetwork,
}

// Schema
sea_query::Table::create()
.table(host_network::Entity)
.col(ColumnDef::new(host_network::Column::Id).integer().not_null().auto_increment().primary_key())
.col(ColumnDef::new(host_network::Column::Ipaddress).inet().not_null())
.col(ColumnDef::new(host_network::Column::Network).cidr().not_null())
.to_owned();

// CRUD
host_network::ActiveModel {
ipaddress: Set(IpNetwork::new(Ipv6Addr::new(..))),
network: Set(IpNetwork::new(Ipv4Addr::new(..))),
..Default::default()
}

Added default_values to ActiveModelTrait​

The ActiveModel::default() returns ActiveModel { .. NotSet } by default (it can also be overridden).

We've added a new method default_values() which would set all fields to their actual Default::default() values.

This fills in a gap in the type system to help with serde. A real-world use case is to improve ActiveModel::from_json, an upcoming new feature (which is a breaking change, sadly).

#[derive(DeriveEntityModel)]
#[sea_orm(table_name = "fruit")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub cake_id: Option<i32>,
pub type_without_default: active_enums::Tea,
}

assert_eq!(
fruit::ActiveModel::default_values(),
fruit::ActiveModel {
id: Set(0), // integer
name: Set("".into()), // string
cake_id: Set(None), // option
type_without_default: NotSet, // not available
},
);

If you are interested in how this works under the hood, a new method Value::dummy_value is added in SeaQuery:

use sea_orm::sea_query::Value;
let v = Value::Int(None);
let n = v.dummy_value();
assert_eq!(n, Value::Int(Some(0)));

The real magic happens with a set of new traits, DefaultActiveValue, DefaultActiveValueNone and DefaultActiveValueNotSet, and taking advantage of Rust's autoref specialization mechanism used by anyhow:

use sea_orm::value::{DefaultActiveValue, DefaultActiveValueNone, DefaultActiveValueNotSet};

let v = (&ActiveValue::<i32>::NotSet).default_value();
assert_eq!(v, ActiveValue::Set(0));

let v = (&ActiveValue::<Option<i32>>::NotSet).default_value();
assert_eq!(v, ActiveValue::Set(None));

let v = (&ActiveValue::<String>::NotSet).default_value();
assert_eq!(v, ActiveValue::Set("".to_owned()));

let v = (&ActiveValue::<Option<String>>::NotSet).default_value();
assert_eq!(v, ActiveValue::Set(None));

let v = (&ActiveValue::<TimeDateTime>::NotSet).default_value();
assert!(matches!(v, ActiveValue::Set(_)));

This enables progressive enhancements based on the traits of the individual ActiveValue type.

Make sea-orm-cli & sea-orm-migration dependencies optional​

#2367

Some engineering teams prefer vendoring sea-orm-cli into their own project as part of the cargo workspace, and so would like to have more control of the dependency graph. This change makes it possible to pick the exact features needed by your project.

Enhancements​

  • Impl IntoCondition for RelationDef #2587
    This allows using RelationDef directly where the query API expects an IntoCondition
let query = Query::select()
.from(fruit::Entity)
.inner_join(cake::Entity, fruit::Relation::Cake.def())
.to_owned();

assert_eq!(
query.to_string(MysqlQueryBuilder),
r#"SELECT FROM `fruit` INNER JOIN `cake` ON `fruit`.`cake_id` = `cake`.`id`"#
);
assert_eq!(
query.to_string(PostgresQueryBuilder),
r#"SELECT FROM "fruit" INNER JOIN "cake" ON "fruit"."cake_id" = "cake"."id""#
);
assert_eq!(
query.to_string(SqliteQueryBuilder),
r#"SELECT FROM "fruit" INNER JOIN "cake" ON "fruit"."cake_id" = "cake"."id""#
);
  • Use fully-qualified syntax for ActiveEnum associated type #2552
  • Added try_getable_postgres_array!(Vec<u8>) (to support bytea[]) #2503
  • Accept LikeExpr in like and not_like #2549
  • Loader: retain only unique key values in the query condition #2569
  • Add proxy transaction impl #2573
  • Relax TransactionError's trait bound for errors to allow anyhow::Error #2602
  • [sea-orm-cli] Fix PgVector codegen #2589
  • [sea-orm-cli] Support postgres array in expanded format #2545

Bug fixes​

  • Quote type properly in AsEnum casting #2570
assert_eq!(
lunch_set::Entity::find()
.select_only()
.column(lunch_set::Column::Tea)
.build(DbBackend::Postgres)
.to_string(),
r#"SELECT CAST("lunch_set"."tea" AS "text") FROM "lunch_set""#
// "text" is now quoted; will work for "text"[] as well
);
  • Include custom column_name in DeriveColumn Column::from_str impl #2603
#[derive(DeriveEntityModel)]
pub struct Model {
#[sea_orm(column_name = "lAsTnAmE")]
last_name: String,
}

assert!(matches!(Column::from_str("lAsTnAmE").unwrap(), Column::LastName));
  • Check if url is well-formed before parsing (avoid panic) #2558
let db = Database::connect("postgre://sea:sea@localhost/bakery").await?;
// note the missing `s`; results in `DbErr::Conn`
  • QuerySelect::column_as method cast ActiveEnum column #2551
#[derive(Debug, FromQueryResult, PartialEq)]
struct SelectResult {
tea_alias: Option<Tea>,
}

assert_eq!(
SelectResult {
tea_alias: Some(Tea::EverydayTea),
},
Entity::find()
.select_only()
.column_as(Column::Tea, "tea_alias")
.into_model()
.one(db)
.await?
.unwrap()
);
  • Fix unicode string enum #2218
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
pub enum SeaORM {
#[sea_orm(string_value = "μ”¨μ˜€μ•Œμ— ")]
μ”¨μ˜€μ•Œμ— ,
}

Upgrades​

  • Upgrade sqlx to 0.8.4 #2562
  • Upgrade sea-query to 0.32.5
  • Upgrade sea-schema to 0.16.2
  • Upgrade heck to 0.5 #2218

House Keeping​

  • Replace once_cell crate with std equivalent #2524 (available since rust 1.80)

Release Planning​

SeaORM 1.0 is a stable release. As demonstrated, we are able to ship many new features without breaking the API. The 1.1 version will be maintained until October 2025, and we'll likely release a 1.2 version with some breaking changes afterwards.

The underlying library SeaQuery will undergo an overhaul and be promoted to 1.0.

If you have suggestions on breaking changes, you are welcome to post them in the discussions:

SQL Server Support​

We've been beta-testing SQL Server for SeaORM for a while. SeaORM X offers the same SeaORM API for MSSQL. We ported all test cases and most examples, complemented by MSSQL specific documentation. If you are building enterprise software for your company, you can request commercial access.

Features:

  • SeaQuery + SeaSchema
  • Entity generation with sea-orm-cli
  • GraphQL with Seaography
  • Nested transaction, connection pooling and multi-async runtime

πŸ–₯️ SeaORM Pro: Professional Admin Panel​

SeaORM Pro is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have!

Features:

  • Full CRUD
  • Built on React + GraphQL
  • Built-in GraphQL resolver
  • Customize the UI with simple TOML

Getting Started​

Latest features​

The latest release of SeaORM Pro has a new feature, Model Editor. Instead of updating data in a pop-up dialog, editor offers a dedicated page with deep link to view and update data. It also offers more control to the UI layout.

If you feel generous, a small donation will be greatly appreciated, and goes a long way towards sustaining the organization.

A big shout out to our GitHub sponsors πŸ˜‡:

Gold Sponsors​

QDX pioneers quantum dynamics–powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. We're grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications.

GitHub Sponsors​

Numeus

Caido

Data Intuitive

Marcus Buffett

MS

wh7f


Afonso Barracha

Dean Sheather

KallyDev

Manfred Lee

MasakiMiyazaki