DEV Community

SeaQL
SeaQL

Posted on

The road to SeaQuery 1.0

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:

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.

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
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
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`
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode
  - Alias::new(format!("r{i}")).into_iden()
  + format!("r{i}").into_iden()
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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!(),
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(),
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

.column(ColumnRef::Asterisk(None))
Enter fullscreen mode Exit fullscreen mode

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

Expr::column("id")
Enter fullscreen mode Exit fullscreen mode

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(),
    ]
);
Enter fullscreen mode Exit fullscreen mode

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()])
);
Enter fullscreen mode Exit fullscreen mode

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(" ")
);
Enter fullscreen mode Exit fullscreen mode

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(" ")
);
Enter fullscreen mode Exit fullscreen mode

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> {}
Enter fullscreen mode Exit fullscreen mode

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(" ");
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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),
}
Enter fullscreen mode Exit fullscreen mode

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.

🦀 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

Top comments (0)