The road to SeaQuery 1.0

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:
version | date | notes |
---|---|---|
0.1.0 | 2020-12-16 | initial release |
0.16.0 | 2021-09-02 | SeaORM 0.1 |
0.30.0 | 2023-07-20 | SeaORM 0.12 |
0.31.0 | 2024-08-02 | SeaORM 1.0 |
0.32.0 | 2024-10-17 | SeaORM 1.1 |
0.32.7 | 2025-08-06 | latest 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.
Unify 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. That worsened the duplication even further, bloating the codebase by ~1300 lines of code.
Later, we looked into the Expr
vs SimpleExpr
distinction. It turned out that Expr
was primarily meant to be a "namespace" of static constructors for SimpleExpr
, similar to Func
vs FunctionCall
. But unlike Func
, which is a unit struct, Expr
was given its own data fields, which turned out to be a mistake and led users to pass around Expr
s instead of SimpleExpr
s.
In 1.0, SimpleExpr
is "merged into" Expr
, meaning that SimpleExpr
is now just a type alias: type SimpleExpr = Expr;
. Both names can be used interchangeably. A lot of redundant .into()
can now be removed. 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.
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 given us back those 1300 lines of code.
Potential compile errors
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`
Revamp 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:
- Type info is erased from
Iden
early SeaRc
is no longer an alias toRc
/Arc
. As such,Send
/Sync
is removed from the traitIden
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();
Unbox 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 enums
#891
#[non_exhaustive]
are added to all AST 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:
- named parameter:
{a}
injected - nested parameter access:
{b.b}
inner access - 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(" ");
Refactor 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);
Refactor Tokenizer to avoid string allocation
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 lightweight SeaORM users and most changes can be done mechanically. After that, it will be the most exciting release - SeaORM 2.0!
Our 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.
Sponsor
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 engineering workflows.
GitHub Sponsors
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. 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!
