Skip to main content

69 posts tagged with "news"

View All Tags

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:

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,
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:

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,
#[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.

post.rs
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.

comment.rs
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

What's new in SeaORM 1.1.7

· 14 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

Support Postgres Vector

#2500 The popular pgvector extension enables efficient storage and querying of high-dimensional vector data, supporting applications like similarity search, recommendation systems, and other AI tools.

Thanks to the contribution of @28Smiles, PgVector is now integrated nicely into the SeaQL ecosystem (under feature flag postgres-vector).

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

// Schema
sea_query::Table::create()
.table(image_model::Entity.table_ref())
.col(ColumnDef::new(Column::Id).integer().not_null().auto_increment().primary_key())
.col(ColumnDef::new(Column::Embedding).vector(None).not_null())
..

// Insert
ActiveModel {
id: NotSet,
embedding: Set(PgVector::from(vec![1., 2., 3.])),
}
.insert(db)
.await?

Nested Objects in Relational Queries

#2508 #2179 #1716

We now have a good answer to Why SeaORM does not nest objects for parent-child relation!

The latest improvements to the FromQueryResult and DerivePartialModel macros allows you to nest objects easily, simplifying the construction of complex queries.

To illustrate, let's take a look at the Bakery Schema again.

Bakery ERD

As a simple first example, we'd like to select Cake with Bakery:

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

#[derive(FromQueryResult)]
struct Bakery {
#[sea_orm(from_alias = "bakery_id")]
id: i32,
#[sea_orm(from_alias = "bakery_name")]
brand: String,
}

let cake: Cake = cake::Entity::find()
.select_only()
.column(cake::Column::Id)
.column(cake::Column::Name)
.column_as(bakery::Column::Id, "bakery_id")
.column_as(bakery::Column::Name, "bakery_name")
.left_join(bakery::Entity)
.order_by_asc(cake::Column::Id)
.into_model()
.one(db)
.await?
.unwrap();

assert_eq!(
cake,
Cake {
id: 1,
name: "Basque cheesecake".to_string(),
bakery: Some(Bakery {
id: 20,
brand: "Super Baker".to_string(),
})
}
);

Because the tables cake and bakery have some duplicate column names, we'd have to do custom selects. select_only here clears the default select list, and we apply aliases with column_as. Then, in FromQueryResult we use from_alias to map the query result back to the nested struct.

You may wonder if there are ways to not do the alias and mapping? Yes! There's where DerivePartialModel comes into play. The previous example can be written as:

#[derive(DerivePartialModel)] // FromQueryResult is no longer needed
#[sea_orm(entity = "cake::Entity", from_query_result)]
struct Cake {
id: i32,
name: String,
#[sea_orm(nested)]
bakery: Option<Bakery>,
}

#[derive(DerivePartialModel)]
#[sea_orm(entity = "bakery::Entity", from_query_result)]
struct Bakery {
id: i32,
#[sea_orm(from_col = "Name")]
brand: String,
}

// same as previous example, but without the custom selects
let cake: Cake = cake::Entity::find()
.left_join(bakery::Entity)
.order_by_asc(cake::Column::Id)
.into_partial_model()
.one(db)
.await?
.unwrap();

Under the hood, bakery_ prefix will be added to the column alias in the SQL query.

SELECT
"cake"."id" AS "id",
"cake"."name" AS "name",
"bakery"."id" AS "bakery_id",
"bakery"."name" AS "bakery_brand"
FROM "cake"
LEFT JOIN "bakery" ON "cake"."bakery_id" = "bakery"."id"
ORDER BY "cake"."id" ASC LIMIT 1

Now, let's look at one more advanced three-way join. Our join tree starts from Order:

Order -> Customer
-> LineItem -> Cake
#[derive(Debug, DerivePartialModel, PartialEq)]
#[sea_orm(entity = "order::Entity", from_query_result)]
struct Order {
id: i32,
total: Decimal,
#[sea_orm(nested)]
customer: Customer,
#[sea_orm(nested)]
line: LineItem,
}

#[derive(Debug, DerivePartialModel, PartialEq)]
#[sea_orm(entity = "customer::Entity", from_query_result)]
struct Customer {
name: String,
}

#[derive(Debug, DerivePartialModel, PartialEq)]
#[sea_orm(entity = "lineitem::Entity", from_query_result)]
struct LineItem {
price: Decimal,
quantity: i32,
#[sea_orm(nested)]
cake: Cake,
}

#[derive(Debug, DerivePartialModel, PartialEq)]
#[sea_orm(entity = "cake::Entity", from_query_result)]
struct Cake {
name: String,
}

let items: Vec<Order> = order::Entity::find()
.left_join(customer::Entity)
.left_join(lineitem::Entity)
.join(JoinType::LeftJoin, lineitem::Relation::Cake.def())
.order_by_asc(order::Column::Id)
.order_by_asc(lineitem::Column::Id)
.into_partial_model()
.all(db)
.await?;

assert_eq!(
items,
[
Order {
id: 101,
total: Decimal::from(10),
customer: Customer {
name: "Bob".to_owned()
},
line: LineItem {
cake: Cake {
name: "Cheesecake".to_owned()
},
price: Decimal::from(2),
quantity: 2,
}
},
..
]
);

That's it! Hope you like these new features, and a huge thanks to @Goodjooy for laying the foundation, @jreppnow for implementing the nested logic, and everyone who participated in the discussion.

Bonus: PartialModel -> ActiveModel

#2517 DerivePartialModel got another extension to derive IntoActiveModel as well. Absent attributes will be filled with NotSet. This allows you to have a cake and eat it!

#[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()
}
);

Three way select

#2518 With PartialModel being so powerful, if you still need to do non-nested selects, there's SelectThree, an extension to SelectTwo:

Order -> Lineitem -> Cake
let items: Vec<(order::Model, Option<lineitem::Model>, Option<cake::Model>)> =
order::Entity::find()
.find_also_related(lineitem::Entity)
.and_also_related(cake::Entity)
.order_by_asc(order::Column::Id)
.order_by_asc(lineitem::Column::Id)
.all(db)
.await?;

Insert heterogeneous models

#2433 Insert many now allows active models to have different column sets (it previously panics). Missing columns will be filled with NULL. This makes seeding data (e.g. with Loco) 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)"#,
);

Improved Seaography Integration

#2403 We've simplified the code by allowing you to register entities into Seaography's GraphQL schema directly within the entity module.

pub mod prelude;

pub mod sea_orm_active_enums;

pub mod baker;
pub mod bakery;
pub mod cake;
pub mod cakes_bakers;

seaography::register_entity_modules!([
baker,
bakery,
cake,
cakes_bakers,
]);

seaography::register_active_enums!([
sea_orm_active_enums::Tea,
]);

Enhancements

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

assert_eq!(
cakes_bakers::Entity::insert_many([
cakes_bakers::ActiveModel {
cake_id: Set(1),
baker_id: Set(2),
},
cakes_bakers::ActiveModel {
cake_id: Set(2),
baker_id: Set(1),
},
])
.exec_with_returning_keys(db)
.await
.unwrap(),
[(1, 2), (2, 1)]
);
  • Added DeleteOne::exec_with_returning & DeleteMany::exec_with_returning #2432
  • Support complex type path in DeriveIntoActiveModel #2517
#[derive(DeriveIntoActiveModel)]
#[sea_orm(active_model = "<fruit::Entity as EntityTrait>::ActiveModel")]
struct Fruit {
cake_id: Option<Option<i32>>,
}
  • Added DatabaseConnection::close_by_ref #2511
pub async fn close(self) -> Result<(), DbErr> { .. } // existing
pub async fn close_by_ref(&self) -> Result<(), DbErr> { .. } // new
  • Added Schema::json_schema_from_entity to construct schema metadata for Entities
assert_eq!(
Schema::new(DbBackend::MySql).json_schema_from_entity(lunch_set::Entity),
json! {
"columns": [
{
"name": "id",
"nullable": false,
"type": "integer"
},
{
"name": "name",
"nullable": false,
"type": "string"
},
{
"name": "tea",
"nullable": false,
"type": {
"name": "tea",
"variants": [
"EverydayTea",
"BreakfastTea"
]
}
}
],
"primary_key": [
"id"
]
}
);
  • Construct DatabaseConnection directly from sqlx::PgPool, sqlx::SqlitePool and sqlx::MySqlPool #2348
// these are implemented:
impl From<MySqlPool> for SqlxMySqlPoolConnection { .. }
impl From<MySqlPool> for DatabaseConnection { .. }

// so this is now possible:
let db: DatabaseConnection = mysql_pool.into();
  • Expose underlying row types (e.g. sqlx::postgres::PgRow) #2265
  • [sea-orm-migration] Allow modifying the connection in migrations #2397
#[async_std::main]
async fn main() {
cli::run_cli_with_connection(migration::Migrator, |connect_options| async {
let db = Database::connect(connect_options).await?;
if db.get_database_backend() == DatabaseBackend::Sqlite {
register_sqlite_functions(&db).await;
}
Ok(db)
}).await;
}
  • [sea-orm-cli] Added MIGRATION_DIR environment variable #2419
  • [sea-orm-cli] Added acquire-timeout option #2461
  • [sea-orm-cli] Added impl-active-model-behavior option #2487
  • [sea-orm-cli] Added with-prelude option #2322
    • all: the default value (current behaviour), will generate prelude.rs and add it to mod.rs / lib.rs
    • all-allow-unused-imports: will generate prelude.rs and add it to mod.rs, plus adding #![allow(unused_imports)] in the module
    • none: will not generate prelude.rs

Upgrades

  • Upgrade sqlx to 0.8 #2305 #2371
  • Upgrade bigdecimal to 0.4 #2305
  • Upgrade sea-query to 0.32.0 #2305
  • Upgrade sea-query-binder to 0.7 #2305
  • Upgrade sea-schema to 0.16 #2305
  • Upgrade ouroboros to 0.18 #2353

House Keeping

  • Cleanup legacy ActiveValue::Set #2515
  • Remove futures crate, replace with futures-util #2466

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.x version will be updated until at least October 2025, and we'll decide whether to release a 2.0 version or extend the 1.x life cycle.

SQL Server Support

We've been beta-testing SQL Server for SeaORM for a while. If you are building software for your company, please request early access.

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 😇:

Numeus

Caido

Marcus Buffett


Maxwell Koo

Spencer Ferris

Dean Sheather

Naoki Ikeguchi

Data Intuitive

Marlon Mueller

MasakiMiyazaki

Manfred Lee

KallyDev

Afonso Barracha

gnuphie

Wait... there's one more thing

"Are we web yet?" is a recurring question in the Rust community, the answer is yes, yes, YES!

If you are looking for a batteries-included full-stack web development framework that is strongly-typed, asynchronous, robust and high-performance, look no further than Rust + Loco + SeaQL. We highly recommend giving Loco a try - "It’s Like Ruby on Rails, but for Rust."

With this final piece of software, my vision for a complete full-stack Rust environment is now realized. After years of development in SeaORM + Seaography, I am excited to introduce it to you:

🖥️ 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

Learn More

Tutorial: Modeling Inheritance in SeaORM

· 7 min read
SeaQL Team
Chris Tsang

Introduction

This tutorial walks you through the design and implementation of a REST API endpoint that involves some complex relational queries.

The API looks like this:

POST /api/v1/complex-products

Parameters (JSON body):

FieldTypeDescription
idint[]Get products with these ids
namestringSearch products with name matching this
typeenum of ProductTypeLimit products to this type

Return example:

{
"id": 1,
"name": "Mountain Bike V2",
"type": "Bike",
"price": "2500.0",
"lot_size": "1.0",
"date_added": "2020-01-01T00:00:00",
"last_modified": "2025-01-02T12:18:54",
"history": [
{
"from": "2020-01-01T00:00:00",
"until": "2022-01-01T00:00:00",
"name": "Mountain Bike V1"
}
]
}

Schema

Imagine we run a store with many types of products. Each product type has its own attributes and we want to factor out the common product attributes to a base class.

In OOP terms:

struct BaseProduct {
r#type: ProductType,
..
}

// made up syntax, but it means inheritance
struct ComplexProduct: BaseProduct { .. }

enum ProductType { .. }

In SQL terms, we have 3 entities and 2 relations among them:

  • BaseProduct -> ComplexProduct
  • BaseProduct -> ProductType

Below are the SeaORM Entities:

BaseProduct

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "base_product")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
#[sea_orm(unique)]
pub name: String,
pub type_id: i32, // linking to product_type
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_one = "super::complex_product::Entity")]
ComplexProduct,
#[sea_orm(has_many = "super::product_history::Entity")]
ProductHistory,
#[sea_orm(
belongs_to = "super::product_type::Entity",
from = "Column::TypeId",
to = "super::product_type::Column::Id",
on_update = "NoAction",
on_delete = "NoAction"
)]
ProductType,
}

ComplexProduct

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "complex_product")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub product_id: i64, // linking to base_product
#[sea_orm(column_type = "Decimal(Some((30, 15)))", nullable)]
pub price: Option<Decimal>,
#[sea_orm(column_type = "Decimal(Some((30, 15)))", nullable)]
pub lot_size: Option<Decimal>,
pub date_added: DateTime,
pub last_modified: DateTime,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::base_product::Entity",
from = "Column::ProductId",
to = "super::base_product::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
BaseProduct,
}

ProductType

Basically an 'enum table'.

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

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::base_product::Entity")]
BaseProduct,
}

1. Define result data structure

#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize)]
pub struct ComplexProduct {
pub id: i64,
pub name: String,
pub r#type: String,
pub price: Decimal,
pub lot_size: Decimal,
pub date_added: DateTime,
pub last_modified: DateTime,
#[sea_orm(skip)]
pub history: Vec<product_history::Model>,
}

With Serialize, you can transform the select result into JSON directly.

2. Define helper aliases

#[derive(DeriveIden, Clone, Copy)]
pub struct Id;

#[derive(DeriveIden, Clone, Copy)]
pub struct Name;

#[derive(DeriveIden, Clone, Copy)]
pub struct Base;

use complex_product::Entity as Prod;
pub type ProdCol = <Prod as EntityTrait>::Column;
type ProdRel = <Prod as EntityTrait>::Relation;

This would make our code much more concise and readable.

Avoid using Alias::new because it's error-prone and slightly more expensive.

3. Custom selects

fn query() -> Select<complex_product::Entity> {
complex_product::Entity::find()
.select_only()
.tbl_col_as((Base, Id), "id")
.tbl_col_as((Base, Name), "name")
.column_as(product_type::Column::Name, "type")
.column_as(ProdCol::Price, "price")
.column_as(ProdCol::LotSize, "lot_size")
.column_as(ProdCol::DateAdded, "date_added")
.column_as(ProdCol::LastModified, "last_modified")
.join_as(JoinType::InnerJoin, ProdRel::BaseProduct.def(), Base)
.join(JoinType::InnerJoin, base_product::Relation::ProductType.def().from_alias(Base))
.order_by_asc(Expr::col((Base, Id)))
}

Our query starts from ComplexProduct. We join back to BaseProduct, alias it as Base. We then join to ProductType via Base:

ComplexProduct -> BaseProduct as Base -> ProductType

column_as automatically prefix the column with the table name. from_alias is doing the magic here, allowing us to reuse the existing Relation by overwriting the left hand side of the on condition.

You can use the join method to construct complex joins in select queries. It takes any RelationDef, and you can further customize the join conditions. You can find more examples here.

4. Filter Conditions

Let's define struct for query parameters. Again, using serde here so it can be deserialized from JSON.

#[derive(Default, Deserialize)]
pub struct Query {
#[serde(default)]
pub id: Vec<i64>, // if unspecified, will be an empty vec
pub name: Option<String>,
pub r#type: Option<String>,
}

Then, we transform the parameters into SQL where conditions:

fn condition(query: Query) -> Condition {
Condition::all()
.add_option(if !query.id.is_empty() {
Some(Expr::col((Base, Id)).is_in(query.id))
} else { None })
.add_option(if let Some(name) = &query.name {
Some(Expr::col((Base, Name)).like(name))
} else { None })
.add_option(if let Some(r#type) = &query.r#type {
Some(product_type::Column::Name.eq(r#type))
} else { None })
}

Bonus tip: if you're only using Postgres you can replace is_in with any:

use sea_orm::sea_query::extension::postgres::PgFunc;

Expr::col((Base, Id)).eq(PgFunc::any(query.id)) // WHERE base.id = ANY($N)

Combining the above functions, here is how we implement the API endpoint:

pub async fn query_products(db: DbConn, q: Query)
-> Result<Vec<ComplexProduct>, DbErr>
{
query()
.filter(condition(q))
.into_model::<ComplexProduct>()
.all(&db)
.await
}

5. Associated models

Suppose we have a data structure associated with each BaseProduct recording its history.

ProductHistory

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize)]
#[sea_orm(table_name = "product_history")]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip)]
pub id: i32,
#[serde(skip)]
pub product_id: i64,
pub from: DateTime,
pub until: DateTime,
pub name: Option<String>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::base_product::Entity",
from = "Column::ProductId",
to = "super::base_product::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
BaseProduct,
}

Let's make a helper function to query the histories associated to a set of products:

fn history_of(ids: Vec<i64>) -> Select<product_history::Entity> {
product_history::Entity::find()
.filter(Expr::col(product_history::Column::ProductId).is_in(ids))
.order_by_asc(product_history::Column::Id)
}

let histories = history_of(products.iter().map(|s| s.id).collect::<Vec<_>>())
.all(&db)
.await?;

The final step is to associate product_history::Model to ComplexProduct:

// parent should be unique and already ordered by id.
fn associate(
mut parent: Vec<ComplexProduct>,
children: Vec<product_history::Model>,
) -> Vec<ComplexProduct> {
let len = parent.len();
parent.dedup_by_key(|s| s.id);
if len != parent.len() {
warn!("parent is not unique.");
}

let parent_id_map: HashMap<i64, usize> = parent
.iter()
.enumerate()
.map(|(i, s)| (s.id, i))
.collect();

// put children into associated parent
for item in children {
if let Some(index) = parent_id_map.get(&item.product_id) {
parent[*index].history.push(item);
}
}

parent
}

let products = associate(products, histories);

This is sometimes called "data loader" pattern, and can be generalized with generics to work with any models.

Conclusion

SeaORM's type system encourages you to write modular and reusable code, embracing the "Don't repeat yourself" principle.

You define the Entities and Relations once.

You define the aliases and query helpers once.

You can pass the Select<T> and Condition around.

You then assemble these pieces together to implement any complex API!

🦀 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. Stick them on your laptop, notebook, or any gadget to show off your love for Rust!

Moreover, all proceeds contributes directly to the ongoing development of SeaQL projects.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography, FireDBG
  • Mascot of SeaQL: Terres the Hermit Crab
  • Mascot of Rust: Ferris the Crab
  • The Rustacean word

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

SeaQL Community Survey 2024 Results

· 5 min read
Billy Chan
SeaQL Team

695 members of the SeaQL community from 47 countries kindly contributed their thoughts on using SeaQL libraries, learning Rust and employing Rust in their day to day development lives. From these responses we hope to get an understanding of where the SeaQL and Rust community stands in 2024.

This is our first community survey, we will conduct the survey annually to keep track of how the community evolves over time.

Developer Profile

Q. Where are you located in?

Participants are from 47 countries across the world!

Other: ArmeniaAzerbaijanBangladeshCambodiaColombia CroatiaCzech RepublicDenmarkEgyptFinlandGuatemalaIsraelItalyKazakhstanKenyaNepalRomaniaSaudi ArabiaSlovakiaSloveniaSouth AfricaSpainSwedenSyriaTurkey

Q. Including any education, how many years have you been coding in total?

Q. Which of the following describes your current job, the one you do most of the time?

Other: Academic researcherMarketing or sales professionalEngineer, dataData or business analystSolution ArchitectDeveloper, QA or testDesignerDeveloper, game or graphicsHobbyist DeveloperEducator

Using SeaQL Libraries in Building a Project

Q. Roughly how long have you been using SeaQL libraries?

Q. Which SeaQL libraries are you using?

Q. Are you using SeaQL libraries in a personal, academic or professional context?

Q. How many team members (including you) are working on the project?

Q. Can you categorize the nature of the project?

Other: Backend for a Web 3.0 ApplicationIoT ApplicationBackend SystemA Document GeneratorMobile ProjectSaaSDatabase for a Cybersecurity SoftwareAPI学习项目Backend for a Multiplatform Application (Using Tauri)Frontend + Backend Work

Q. What is your development environment?

Linux Breakdown

Windows Breakdown

macOS Breakdown

Q. Which database(s) do you use?

Other: SurrealDBEdgeDBMariaDBMariaDB Galera ClusterMongoDBlibSQLMSSQLTImescaleYugabyteOracle Database

Q. Which web framework are you using?

Other: SolidJSTauriTonictrilliumVueWarp

Q. What is the deployment environment?

Other: DigitalOcean App PlatformDogYunFly.ioOracle Cloud

Using Rust at Work

Q. Are you using Rust at work?

Q. Which industry your company is in?

Other: Space Situational AwarenessSecurityAdsBettingConstructionAutomotiveLast Mile LogisticsIoT

Q. How many engineers in your company are dedicated to writing Rust?

Q. What is the size of your company?

Q. Which layer(s) of the technology stack are using Rust?

Other: EmbeddedHardware Drivers

Q. Would your company be interested in using SeaQL libraries with other databases?

Learning Rust

Q. Are you learning / new to Rust?

Q. Which language(s) are you most familiar with?

Other: ErlangCobolLuaPerlRPGAssembly

Q. Do you find Rust easy or hard to learn?

Q. What motivates you to learn Rust?

Other: School makes me to learnBetter option to learn than C/C++it's fantasticStability of ecosystem, security, speedThe ease of writing clean code

Q. What learning resources do you rely on?

Q. What is your first project built using Rust?

Q. Are you familiar with SQL?

About SeaQL Libraries

Q. Why did you choose SeaQL libraries?

Other: sea-orm-cli codegenErgonomic APIWord of mouth

Q. Which aspects do you want to see advancement on SeaORM?

Other: Better macros for less boilerplate codeAutomatic migrationSchema diffing for migrations

Q. What tools would you be interested in using, if developed first-party by SeaQL?

Share Your Thoughts

Q. Anything else you want to say?

Didn't expect this section to turn into a testimonial, thank you for all the kind words :)

Love your toolchain, it's awesome to work with!

Wonderful library! To provide some additional feedback: I found myself missing documentation in certain parts of the API, leading to me having to guess what the method actually does for a given database backend (namely in relation to query builders).

Schema/migration management would be really nice. I have used Django in the past and loved the automatic migration file generation.

SeaORM is great - it just replaces existing asp.net entity framework projects at our company.

🦀 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. Stick them on your laptop, notebook, or any gadget to show off your love for Rust!

Moreover, all proceeds contributes directly to the ongoing development of SeaQL projects.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography, FireDBG
  • Mascot of SeaQL: Terres the Hermit Crab
  • Mascot of Rust: Ferris the Crab
  • The Rustacean word

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

What's new in SeaQuery 0.32.x

· 7 min read

🎉 We are pleased to release SeaQuery 0.32.0 / 0.32.1! Here are some feature highlights 🌟:

New Features

Unify Expr and SimpleExpr Methods with ExprTrait #791

Previously, "operator" methods (e.g. add, eq) are duplicated across Expr and SimpleExpr, but the list of methods is slightly different for each. And since Expr and SimpleExpr are distinct types, it makes writing generic code difficult.

The ExprTrait looks like this:

pub trait ExprTrait: Sized {
// Required methods
fn as_enum<N>(self, type_name: N) -> SimpleExpr
where N: IntoIden;
fn binary<O, R>(self, op: O, right: R) -> SimpleExpr
where O: Into<BinOper>,
R: Into<SimpleExpr>;
fn cast_as<N>(self, type_name: N) -> SimpleExpr
where N: IntoIden;
fn unary(self, o: UnOper) -> SimpleExpr;

// Provided methods
fn add<R>(self, right: R) -> SimpleExpr where R: Into<SimpleExpr> { ... }
fn and<R>(self, right: R) -> SimpleExpr where R: Into<SimpleExpr> { ... }
fn between<A, B>(self, a: A, b: B) -> SimpleExpr
where A: Into<SimpleExpr>,
B: Into<SimpleExpr> { ... }
fn div<R>(self, right: R) -> SimpleExpr where R: Into<SimpleExpr> { ... }
fn eq<R>(self, right: R) -> SimpleExpr where R: Into<SimpleExpr> { ... }
fn equals<C>(self, col: C) -> SimpleExpr where C: IntoColumnRef { ... }

// omitting the where clause below to make it more concise ..

fn gt<R>(self, right: R) -> SimpleExpr;
fn gte<R>(self, right: R) -> SimpleExpr;
fn in_subquery(self, sel: SelectStatement) -> SimpleExpr;
fn in_tuples<V, I>(self, v: I) -> SimpleExpr;
fn is<R>(self, right: R) -> SimpleExpr;
fn is_in<V, I>(self, v: I) -> SimpleExpr;
fn is_not<R>(self, right: R) -> SimpleExpr;
fn is_not_in<V, I>(self, v: I) -> SimpleExpr;
fn is_not_null(self) -> SimpleExpr;
fn is_null(self) -> SimpleExpr;
fn left_shift<R>(self, right: R) -> SimpleExpr;
fn like<L>(self, like: L) -> SimpleExpr;
fn lt<R>(self, right: R) -> SimpleExpr;
fn lte<R>(self, right: R) -> SimpleExpr;
fn modulo<R>(self, right: R) -> SimpleExpr;
fn mul<R>(self, right: R) -> SimpleExpr;
fn ne<R>(self, right: R) -> SimpleExpr;
fn not(self) -> SimpleExpr;
fn not_between<A, B>(self, a: A, b: B) -> SimpleExpr;
fn not_equals<C>(self, col: C) -> SimpleExpr;
fn not_in_subquery(self, sel: SelectStatement) -> SimpleExpr;
fn not_like<L>(self, like: L) -> SimpleExpr;
fn or<R>(self, right: R) -> SimpleExpr;
fn right_shift<R>(self, right: R) -> SimpleExpr;
fn sub<R>(self, right: R) -> SimpleExpr;
fn bit_and<R>(self, right: R) -> SimpleExpr;
fn bit_or<R>(self, right: R) -> SimpleExpr;
}
  • Added ExprTrait to unify Expr and SimpleExpr methods
  • Added impl<T> ExprTrait for T where T: Into<SimpleExpr> to maintain backwards compatibility for all Into<SimpleExpr> types, such as Value and FunctionCall
  • Added trait PgExpr: ExprTrait: database specific expression for Postgres and impl PgExpr for FunctionCall, ColumnRef, Keyword, LikeExpr, Value
  • Added trait SqliteExpr: ExprTrait: database specific expression for SQLite and impl SqliteExpr for FunctionCall, ColumnRef, Keyword, LikeExpr, Value

Support of Postgres Vector #774

Example:

assert_eq!(
Query::select()
.columns([Char::Character])
.from(Char::Table)
.and_where(
Expr::col(Char::Character).eq(Expr::val(pgvector::Vector::from(vec![1.0, 2.0])))
)
.to_string(PostgresQueryBuilder),
r#"SELECT "character" FROM "character" WHERE "character" = '[1,2]'"#
);

Support Partial Index #478

  • Support partial index CREATE INDEX .. WHERE ..

Example (Postgres):

assert_eq!(
Index::create()
.unique()
.nulls_not_distinct()
.name("partial-index-glyph-image-not-null")
.table(Glyph::Table)
.col(Glyph::Image)
.and_where(Expr::col(Glyph::Image).is_not_null())
.to_string(PostgresQueryBuilder),
r#"CREATE UNIQUE INDEX "partial-index-glyph-image-not-null" ON "glyph" ("image") NULLS NOT DISTINCT WHERE "image" IS NOT NULL"#
);

Example (Sqlite):

assert_eq!(
Index::create()
.if_not_exists()
.unique()
.name("partial-index-glyph-image-not-null")
.table(Glyph::Table)
.col(Glyph::Image)
.and_where(Expr::col(Glyph::Image).is_not_null())
.to_string(SqliteQueryBuilder),
r#"CREATE UNIQUE INDEX IF NOT EXISTS "partial-index-glyph-image-not-null" ON "glyph" ("image") WHERE "image" IS NOT NULL"#
);

Get Null Value

This one may seem a little bummer, but it is type system problem. In order to support the Postgres protocol, SeaQuery's Value enum does not have a Null variant. This new Value::as_null method allows you to:

  1. construct a typed null value
  2. nullify a value
  3. define generic functions (impl Into<Value>)
let v = Value::Int(Some(2));
let n = v.as_null();

assert_eq!(n, Value::Int(None));

// one liner:
assert_eq!(Into::<Value>::into(2.2).as_null(), Value::Double(None));

Bitwise AND/OR Operators #841

Examples:

let query = Query::select()
.expr(1.bit_and(2).eq(3))
.to_owned();

assert_eq!(
query.to_string(PostgresQueryBuilder),
r#"SELECT (1 & 2) = 3"#
);
let query = Query::select()
.expr(1.bit_or(2).eq(3))
.to_owned();

assert_eq!(
query.to_string(PostgresQueryBuilder),
r#"SELECT (1 | 2) = 3"#
);

Enhancements

  • #817 Replaced Educe with manual implementations
    • This is an effort to cut down compilation time
  • #844 Added GREATEST & LEAST function
  • #836 Added ValueType::enum_type_name()
  • #835 Removed "one common table" restriction on recursive CTE

sea-query-derive

We've finally done it! Removing the last bit of syn v1 from our dependency tree:

sea-query % cargo tree |grep 'syn '
│ └── syn v2.0.39
│ │ └── syn v2.0.39 (*)
│ └── syn v2.0.39 (*)
├── syn v2.0.39 (*)
└── syn v2.0.39 (*)
│ │ └── syn v2.0.39 (*)
  • Merged #[enum_def] into sea-query-derive
  • #769 #[enum_def] now impl additional IdenStatic and AsRef<str>

sea-query-attr

We've merged this crate into sea-query-derive, and they will be maintained together from now on.

  • Updated syn, heck and darling
  • sea-query-attr is now deprecated

Upgrades

  • #798 Upgraded sqlx to 0.8
  • #798 Upgraded bigdecimal to 0.4
  • #802 Upgraded rusqlite to 0.32

Integration Examples

SeaQuery plays well with the other crates in the rust ecosystem.

Community

SeaQL.org is an independent open-source organization run by passionate ️developers. If you like our projects, please star ⭐ and share our repositories. If you feel generous, a small donation via GitHub Sponsor will be greatly appreciated, and goes a long way towards sustaining the organization 🚢.

SeaQuery is a community driven project. We welcome you to participate, contribute and together build for Rust's future 🦀.

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. Stick them on your laptop, notebook, or any gadget to show off your love for Rust!

Moreover, all proceeds contributes directly to the ongoing development of SeaQL projects.

Sticker Pack Contents:

  • Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography, FireDBG
  • Mascot of SeaQL: Terres the Hermit Crab
  • Mascot of Rust: Ferris the Crab
  • The Rustacean word

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL