Skip to main content

The road to SeaQuery 1.0

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

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 Exprs instead of SimpleExprs.

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:

  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

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:

  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(" ");

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!

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

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.


Chris Tsang
Maintainer
Dmitrii Aleksandrov
Maintainer
Huliiiiii
Contributor

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