Skip to main content

Call for Contributors and Reviewers πŸ“’

Β· 5 min read
Chris Tsang
SeaQL Founder

We are calling for contributors and reviewers for SeaQL projects πŸ“’!

The SeaQL userbase has been steadily growing in the past year, and it’s a pleasure for us to have helped individuals and start-ups to build their projects in Rust. However, the volume of questions, issues and pull requests is nearly saturating our core members’ capacity.

But again, thank you everyone for participating in the community!

If your project depends on SeaQL and you want to help us, here are some suggestions (if you have not already, star all our repositories and follow us on Twitter):

  1. Financial Contribution. You can sponsor us on GitHub and those will be used to cover our expenses. As a courtesy, we listen to our sponsors for their needs and use cases, and we also communicate our organizational development from time-to-time.
  2. Code Contribution. Opening a PR with us is always appreciated! To get started, you can go through our issue trackers and pick one to handle. If you are thinking of developing a substantial feature, start with drafting a "Proposal & Implementation Plan" (PIP).
  3. Knowledge Contribution. There are various formats of knowledge sharing: tutorial, cookbook, QnA and Discord. You can open PRs to our documentation repositories or publish on your own. We will be happy to list it in our learning resources section. Keep an eye on our GitHub Discussions and Discord and help others where you can!
  4. Code Review. This is an important process of our engineering. Right now, only 3 of our core members serve as reviewers. Non-core members can also become reviewers and I invite you to become one!

Now, I’d like to outline our review policy: for maturing projects, each PR merged has to be approved by at least two reviewers and one of them must be a core member; self-review allowed. Here are some examples:

  • A core member opened a PR, another core member approved βœ…
  • A core member opened a PR, a reviewer approved βœ…
  • A reviewer opened a PR, a core member approved βœ…
  • A reviewer opened a PR, another reviewer approved β›”
  • A contributor opened a PR, 2 core members approved βœ…
  • A contributor opened a PR, a core member and a reviewer approved βœ…
  • A contributor opened a PR, 2 reviewers approved β›”

In a nutshell, at least two pairs of trusted eyes should have gone through each PR.

What are the criteria when reviewing a PR?​

The following questions should all be answered yes.

  1. Implementation, documentation and tests
    1. Is the implementation easy to follow (have meaningful variable and function names)?
    2. Is there sufficient document to the API?
    3. Are there adequate tests covering various cases?
  2. API design
    1. Is the API self-documenting so users can understand its use easily?
    2. Is the API style consistent with our existing API?
    3. Does the API made reasonable use of the type system to enforce constraints?
    4. Are the failure paths and error messages clear?
    5. Are all breaking changes justified and documented?
  3. Functionality
    1. Does the feature make sense in computer science terms?
    2. Does the feature actually work with all our supported backends?
    3. Are all caveats discussed and eliminated / documented?
  4. Architecture
    1. Does it fit with the existing architecture of our codebase?
    2. Is it not going to create technical debt / maintenance burden?
    3. Does it not break abstraction?

1, 2 & 3 are fairly objective and factual, however the answers to 4 probably require some discussion and debate. If a consensus cannot be made, @tyt2y3 will make the final verdict.

Who are the current reviewers?​

As of today, SeaQL has 3 core members who are also reviewers:

Chris Tsang

Founder. Maintains all projects.

Billy Chan

Founding member. Co-maintainer of SeaORM and Seaography.

Ivan Krivosheev

Joined in 2022. Co-maintainer of SeaQuery.

How to become a reviewer?​

We are going to invite a few contributors we worked closely with, but you can also volunteer – the requirement is: you have made substantial code contribution to our projects, and has shown familiarity with our engineering practices.

Over time, when you have made significant contribution to our organization, you can also become a core member.

Let’s build for Rust's future together πŸ¦€β€‹

What's new in SeaQuery 0.28.0

Β· 6 min read
SeaQL Team
Ivan Krivosheev

πŸŽ‰ We are pleased to release SeaQuery 0.28.0! Here are some feature highlights 🌟:

New IdenStatic trait for static identifier​

[#508] Representing a identifier with &'static str. The IdenStatic trait looks like this:

pub trait IdenStatic: Iden + Copy + 'static {
fn as_str(&self) -> &'static str;
}

You can derive it easily for your existing Iden. Just changing the #[derive(Iden)] into #[derive(IdenStatic)].

#[derive(IdenStatic)]
enum User {
Table,
Id,
FirstName,
LastName,
#[iden = "_email"]
Email,
}

assert_eq!(User::Email.as_str(), "_email");

New PgExpr and SqliteExpr traits for backend specific expressions​

[#519] Postgres specific and SQLite specific expressions are being moved into its corresponding trait. You need to import the trait into scope before construct the expression with those backend specific methods.

// Importing `PgExpr` trait before constructing Postgres expression
use sea_query::{extension::postgres::PgExpr, tests_cfg::*, *};

let query = Query::select()
.columns([Font::Name, Font::Variant, Font::Language])
.from(Font::Table)
.and_where(Expr::val("a").concatenate("b").concat("c").concat("d"))
.to_owned();

assert_eq!(
query.to_string(PostgresQueryBuilder),
r#"SELECT "name", "variant", "language" FROM "font" WHERE 'a' || 'b' || 'c' || 'd'"#
);
// Importing `SqliteExpr` trait before constructing SQLite expression
use sea_query::{extension::sqlite::SqliteExpr, tests_cfg::*, *};

let query = Query::select()
.column(Font::Name)
.from(Font::Table)
.and_where(Expr::col(Font::Name).matches("a"))
.to_owned();

assert_eq!(
query.to_string(SqliteQueryBuilder),
r#"SELECT "name" FROM "font" WHERE "name" MATCH 'a'"#
);

Bug Fixes​

// given
let (statement, values) = sea_query::Query::select()
.column(Glyph::Id)
.from(Glyph::Table)
.cond_where(Cond::any()
.add(Cond::all()) // empty all() => TRUE
.add(Cond::any()) // empty any() => FALSE
)
.build(sea_query::MysqlQueryBuilder);

// old behavior
assert_eq!(statement, r#"SELECT `id` FROM `glyph`"#);

// new behavior
assert_eq!(
statement,
r#"SELECT `id` FROM `glyph` WHERE (TRUE) OR (FALSE)"#
);

// a complex example
let (statement, values) = Query::select()
.column(Glyph::Id)
.from(Glyph::Table)
.cond_where(
Cond::all()
.add(Cond::all().not())
.add(Cond::any().not())
.not(),
)
.build(MysqlQueryBuilder);

assert_eq!(
statement,
r#"SELECT `id` FROM `glyph` WHERE NOT ((NOT TRUE) AND (NOT FALSE))"#
);

Breaking Changes​

  • [#535] MSRV is up to 1.62
# Make sure you're running SeaQuery with Rust 1.62+ πŸ¦€
$ rustup update
  • [#492] ColumnType::Array definition changed from Array(SeaRc<Box<ColumnType>>) to Array(SeaRc<ColumnType>)
  • [#475] Func::* now returns FunctionCall instead of SimpleExpr
  • [#475] Func::coalesce now accepts IntoIterator<Item = SimpleExpr> instead of IntoIterator<Item = Into<SimpleExpr>
  • [#475] Removed Expr::arg and Expr::args - these functions are no longer needed
  • [#507] Moved all Postgres specific operators to PgBinOper
  • [#476] Expr methods used to accepts Into<Value> now accepts Into<SimpleExpr>
  • [#476] Expr::is_in, Expr::is_not_in now accepts Into<SimpleExpr> instead of Into<Value> and convert it to SimpleExpr::Tuple instead of SimpleExpr::Values
  • [#475] Expr::expr now accepts Into<SimpleExpr> instead of SimpleExpr
  • [#519] Moved Postgres specific Expr methods to new trait PgExpr
  • [#528] Expr::equals now accepts C: IntoColumnRef instead of T: IntoIden, C: IntoIden
use sea_query::{*, tests_cfg::*};

let query = Query::select()
.columns([Char::Character, Char::SizeW, Char::SizeH])
.from(Char::Table)
.and_where(
Expr::col((Char::Table, Char::FontId))
- .equals(Font::Table, Font::Id)
+ .equals((Font::Table, Font::Id))
)
.to_owned();

assert_eq!(
query.to_string(MysqlQueryBuilder),
r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`font_id` = `font`.`id`"#
);
  • [#525] Removed integer and date time column types' display length / precision option

API Additions​

  • [#475] Added SelectStatement::from_function
use sea_query::{tests_cfg::*, *};

let query = Query::select()
.column(ColumnRef::Asterisk)
.from_function(Func::random(), Alias::new("func"))
.to_owned();

assert_eq!(
query.to_string(MysqlQueryBuilder),
r#"SELECT * FROM RAND() AS `func`"#
);
  • [#486] Added binary operators from the Postgres pg_trgm extension
use sea_query::extension::postgres::PgBinOper;

assert_eq!(
Query::select()
.expr(Expr::col(Font::Name).binary(PgBinOper::WordSimilarity, Expr::value("serif")))
.from(Font::Table)
.to_string(PostgresQueryBuilder),
r#"SELECT "name" <% 'serif' FROM "font""#
);
  • [#473] Added ILIKE and NOT ILIKE operators
  • [#510] Added the mul and div methods for SimpleExpr
  • [#513] Added the MATCH, -> and ->> operators for SQLite
use sea_query::extension::sqlite::SqliteBinOper;

assert_eq!(
Query::select()
.column(Char::Character)
.from(Char::Table)
.and_where(Expr::col(Char::Character).binary(SqliteBinOper::Match, Expr::val("test")))
.build(SqliteQueryBuilder),
(
r#"SELECT "character" FROM "character" WHERE "character" MATCH ?"#.to_owned(),
Values(vec!["test".into()])
)
);
  • [#497] Added the FULL OUTER JOIN
  • [#530] Added PgFunc::get_random_uuid
  • [#528] Added SimpleExpr::eq, SimpleExpr::ne, Expr::not_equals
  • [#529] Added PgFunc::starts_with
  • [#535] Added Expr::custom_keyword and SimpleExpr::not
use sea_query::*;

let query = Query::select()
.expr(Expr::custom_keyword(Alias::new("test")))
.to_owned();

assert_eq!(query.to_string(MysqlQueryBuilder), r#"SELECT test"#);
assert_eq!(query.to_string(PostgresQueryBuilder), r#"SELECT test"#);
assert_eq!(query.to_string(SqliteQueryBuilder), r#"SELECT test"#);
  • [#539] Added SimpleExpr::like, SimpleExpr::not_like and Expr::cast_as
  • [#532] Added support for NULLS NOT DISTINCT clause for Postgres
  • [#531] Added Expr::cust_with_expr and Expr::cust_with_exprs
use sea_query::{tests_cfg::*, *};

let query = Query::select()
.expr(Expr::cust_with_expr("data @? ($1::JSONPATH)", "hello"))
.to_owned();

assert_eq!(
query.to_string(PostgresQueryBuilder),
r#"SELECT data @? ('hello'::JSONPATH)"#
);
  • [#538] Added support for converting &String to Value

Miscellaneous Enhancements​

  • [#475] New struct FunctionCall which hold function and arguments
  • [#503] Support BigDecimal, IpNetwork and MacAddress for sea-query-postgres
  • [#511] Made value::with_array module public and therefore making NotU8 trait public
  • [#524] Drop the Sized requirement on implementers of SchemaBuilders

Integration Examples​

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

Community​

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

What's new in Seaography 0.3.0

Β· 4 min read
SeaQL Team
Panagiotis Karatakis

πŸŽ‰ We are pleased to release Seaography 0.3.0! Here are some feature highlights 🌟:

Dependency Upgrade​

[#93] We have upgraded a major dependency:

You might need to upgrade the corresponding dependency in your application as well.

Support Self Referencing Relation​

[#99] You can now query self referencing models and the inverse of it.

Self referencing relation should be added to the Relation enum, note that the belongs_to attribute must be belongs_to = "Entity".

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

#[derive(
Clone, Debug, PartialEq, DeriveEntityModel,
async_graphql::SimpleObject, seaography::macros::Filter,
)]
#[sea_orm(table_name = "staff")]
#[graphql(complex)]
#[graphql(name = "Staff")]
pub struct Model {
#[sea_orm(primary_key)]
pub staff_id: i32,
pub first_name: String,
pub last_name: String,
pub reports_to_id: Option<i32>,
}

#[derive(
Copy, Clone, Debug, EnumIter, DeriveRelation,
seaography::macros::RelationsCompact
)]
pub enum Relation {
#[sea_orm(
belongs_to = "Entity",
from = "Column::ReportsToId",
to = "Column::StaffId",
)]
SelfRef,
}

impl ActiveModelBehavior for ActiveModel {}

Then, you can query the related models in GraphQL.

{
staff {
nodes {
firstName
reportsToId
selfRefReverse {
staffId
firstName
}
selfRef {
staffId
firstName
}
}
}
}

The resulting JSON

{
"staff": {
"nodes": [
{
"firstName": "Mike",
"reportsToId": null,
"selfRefReverse": [
{
"staffId": 2,
"firstName": "Jon"
}
],
"selfRef": null
},
{
"firstName": "Jon",
"reportsToId": 1,
"selfRefReverse": null,
"selfRef": {
"staffId": 1,
"firstName": "Mike"
}
}
]
}
}

Web Framework Generator​

[#74] You can generate seaography project with either Actix or Poem as the web server.

CLI Generator Option​

Run seaography-cli to generate seaography code with Actix or Poem as the web framework.

# The command take three arguments, generating project with Poem web framework by default
seaography-cli <DATABASE_URL> <CRATE_NAME> <DESTINATION>

# Generating project with Actix web framework
seaography-cli -f actix <DATABASE_URL> <CRATE_NAME> <DESTINATION>

# MySQL
seaography-cli mysql://root:root@localhost/sakila seaography-mysql-example examples/mysql
# PostgreSQL
seaography-cli postgres://root:root@localhost/sakila seaography-postgres-example examples/postgres
# SQLite
seaography-cli sqlite://examples/sqlite/sakila.db seaography-sqlite-example examples/sqliteql

Actix​

use async_graphql::{
dataloader::DataLoader,
http::{playground_source, GraphQLPlaygroundConfig},
EmptyMutation, EmptySubscription, Schema,
};
use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse};
use sea_orm::Database;
use seaography_example_project::*;
// ...

async fn graphql_playground() -> Result<HttpResponse> {
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(
playground_source(GraphQLPlaygroundConfig::new("http://localhost:8000"))
))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
// ...

let database = Database::connect(db_url).await.unwrap();
let orm_dataloader: DataLoader<OrmDataloader> = DataLoader::new(
OrmDataloader {
db: database.clone(),
},
tokio::spawn,
);

let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription)
.data(database)
.data(orm_dataloader)
.finish();

let app = App::new()
.app_data(Data::new(schema.clone()))
.service(web::resource("/").guard(guard::Post()).to(index))
.service(web::resource("/").guard(guard::Get()).to(graphql_playground));

HttpServer::new(app)
.bind("127.0.0.1:8000")?
.run()
.await
}

Poem​

use async_graphql::{
dataloader::DataLoader,
http::{playground_source, GraphQLPlaygroundConfig},
EmptyMutation, EmptySubscription, Schema,
};
use async_graphql_poem::GraphQL;
use poem::{handler, listener::TcpListener, web::Html, IntoResponse, Route, Server};
use sea_orm::Database;
use seaography_example_project::*;
// ...

#[handler]
async fn graphql_playground() -> impl IntoResponse {
Html(playground_source(GraphQLPlaygroundConfig::new("/")))
}

#[tokio::main]
async fn main() {
// ...

let database = Database::connect(db_url).await.unwrap();
let orm_dataloader: DataLoader<OrmDataloader> = DataLoader::new(
OrmDataloader { db: database.clone() },
tokio::spawn,
);

let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription)
.data(database)
.data(orm_dataloader)
.finish();

let app = Route::new()
.at("/", get(graphql_playground)
.post(GraphQL::new(schema)));

Server::new(TcpListener::bind("0.0.0.0:8000"))
.run(app)
.await
.unwrap();
}

[#84] Filtering, sorting and paginating related 1-to-many queries. Note that the pagination is work-in-progress, currently it is in memory pagination.

For example, find all inactive customers, include their address, and their payments with amount greater than 7 ordered by amount the second result. You can execute the query below at our GraphQL playground.

{
customer(
filters: { active: { eq: 0 } }
pagination: { cursor: { limit: 3, cursor: "Int[3]:271" } }
) {
nodes {
customerId
lastName
email
address {
address
}
payment(
filters: { amount: { gt: "7" } }
orderBy: { amount: ASC }
pagination: { pages: { limit: 1, page: 1 } }
) {
nodes {
paymentId
amount
}
pages
current
pageInfo {
hasPreviousPage
hasNextPage
}
}
}
pageInfo {
hasPreviousPage
hasNextPage
endCursor
}
}
}

Integration Examples​

We have the following examples for you, alongside with the SQL scripts to initialize the database.

Community​

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

What's new in SeaORM 0.10.x

Β· 10 min read
SeaQL Team
Chris Tsang

πŸŽ‰ We are pleased to release SeaORM 0.10.0!

Rust 1.65​

The long-anticipated Rust 1.65 has been released! Generic associated types (GATs) must be the hottest newly-stabilized feature.

How is GAT useful to SeaORM? Let's take a look at the following:

trait StreamTrait<'a>: Send + Sync {
type Stream: Stream<Item = Result<QueryResult, DbErr>> + Send;

fn stream(
&'a self,
stmt: Statement,
) -> Pin<Box<dyn Future<Output = Result<Self::Stream, DbErr>> + 'a + Send>>;
}

You can see that the Future has a lifetime 'a, but as a side effect the lifetime is tied to StreamTrait.

With GAT, the lifetime can be elided:

trait StreamTrait: Send + Sync {
type Stream<'a>: Stream<Item = Result<QueryResult, DbErr>> + Send
where
Self: 'a;

fn stream<'a>(
&'a self,
stmt: Statement,
) -> Pin<Box<dyn Future<Output = Result<Self::Stream<'a>, DbErr>> + 'a + Send>>;
}

What benefit does it bring in practice? Consider you have a function that accepts a generic ConnectionTrait and calls stream():

async fn processor<'a, C>(conn: &'a C) -> Result<...>
where C: ConnectionTrait + StreamTrait<'a> {...}

The fact that the lifetime of the connection is tied to the stream can create confusion to the compiler, most likely when you are making transactions:

async fn do_transaction<C>(conn: &C) -> Result<...>
where C: ConnectionTrait + TransactionTrait
{
let txn = conn.begin().await?;
processor(&txn).await?;
txn.commit().await?;
}

But now, with the lifetime of the stream elided, it's much easier to work on streams inside transactions because the two lifetimes are now distinct and the stream's lifetime is implicit:

async fn processor<C>(conn: &C) -> Result<...>
where C: ConnectionTrait + StreamTrait {...}

Big thanks to @nappa85 for the contribution.


Below are some feature highlights 🌟:

Support Array Data Types in Postgres​

[#1132] Support model field of type Vec<T>. (by @hf29h8sh321, @ikrivosheev, @tyt2y3, @billy1624)

You can define a vector of types that are already supported by SeaORM in the model.

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "collection")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub integers: Vec<i32>,
pub integers_opt: Option<Vec<i32>>,
pub floats: Vec<f32>,
pub doubles: Vec<f64>,
pub strings: Vec<String>,
}

Keep in mind that you need to enable the postgres-array feature and this is a Postgres only feature.

sea-orm = { version = "0.10", features = ["postgres-array", ...] }

Better Error Types​

[#750, #1002] Error types with parsable database specific error. (by @mohs8421, @tyt2y3)

let mud_cake = cake::ActiveModel {
id: Set(1),
name: Set("Moldy Cake".to_owned()),
price: Set(dec!(10.25)),
gluten_free: Set(false),
serial: Set(Uuid::new_v4()),
bakery_id: Set(None),
};

// Insert a new cake with its primary key (`id` column) set to 1.
let cake = mud_cake.save(db).await.expect("could not insert cake");

// Insert the same row again and it failed
// because primary key of each row should be unique.
let error: DbErr = cake
.into_active_model()
.insert(db)
.await
.expect_err("inserting should fail due to duplicate primary key");

match error {
DbErr::Exec(RuntimeErr::SqlxError(error)) => match error {
Error::Database(e) => {
// We check the error code thrown by the database (MySQL in this case),
// `23000` means `ER_DUP_KEY`: we have a duplicate key in the table.
assert_eq!(e.code().unwrap(), "23000");
}
_ => panic!("Unexpected sqlx-error kind"),
},
_ => panic!("Unexpected Error kind"),
}

Run Migration on Any Postgres Schema​

[#1056] By default migration will be run on the public schema, you can now override it when running migration on the CLI or programmatically. (by @MattGson, @nahuakang, @billy1624)

For CLI, you can specify the target schema with -s / --database_schema option:

  • via sea-orm-cli: sea-orm-cli migrate -u postgres://root:root@localhost/database -s my_schema
  • via SeaORM migrator: cargo run -- -u postgres://root:root@localhost/database -s my_schema

You can also run the migration on the target schema programmatically:

let connect_options = ConnectOptions::new("postgres://root:root@localhost/database".into())
.set_schema_search_path("my_schema".into()) // Override the default schema
.to_owned();

let db = Database::connect(connect_options).await?

migration::Migrator::up(&db, None).await?;

Breaking Changes​

enum ColumnType {
// then
Enum(String, Vec<String>)

// now
Enum {
/// Name of enum
name: DynIden,
/// Variants of enum
variants: Vec<DynIden>,
}
...
}
  • A new method array_type was added to ValueType:
impl sea_orm::sea_query::ValueType for MyType {
fn array_type() -> sea_orm::sea_query::ArrayType {
sea_orm::sea_query::ArrayType::TypeName
}
...
}
  • ActiveEnum::name() changed return type to DynIden:
#[derive(Debug, Iden)]
#[iden = "category"]
pub struct CategoryEnum;

impl ActiveEnum for Category {
// then
fn name() -> String {
"category".to_owned()
}

// now
fn name() -> DynIden {
SeaRc::new(CategoryEnum)
}
...
}

SeaORM Enhancements​

CLI Enhancements​

Please check here for the complete changelog.

Integration Examples​

SeaORM plays well with the other crates in the async ecosystem. We maintain an array of example projects for building REST, GraphQL and gRPC services. More examples wanted!

Our GitHub Sponsor profile is up! If you feel generous, a small donation will be greatly appreciated.

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

Γ‰mile Fugulin

Dean Sheather

Shane Sveller

Sakti Dwi Cahyono

Henrik Giesel

Jacob Trueb

Marcus Buffett

Unnamed Sponsor

Unnamed Sponsor

Community​

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

Here is the roadmap for SeaORM 0.11.x.

Toggle Stacked Download Graph in crates.io

Β· 2 min read
Billy Chan
SeaQL Team

Not long ago we opened a PR "Toggle stacked download graph #5010" resolving Convert download chart from stacked chart to regular chart #3876 for crates.io.

What's it all about?

Problem​

The download graph on crates.io used to be a stacked graph. With download count of older versions stack on top of newer versions. You might misinterpret the numbers. Consider this, at the first glance, it seems that version 0.9.2 has 1,500+ downloads on Nov 7. But in fact, it has only 237 downloads that day because the graph is showing the cumulative downloads.

crates.io Stacked Download Graph

This makes it hard to compare the download trend of different versions over time. Why this is important? You may ask. It's important to observe the adoption rate of newer version upon release. This paints a general picture if existing users are upgrading to newer version or not.

Solution​

The idea is simple but effective: having a dropdown to toggle between stacked and unstacked download graph. With this, one can switch between both display mode, comparing the download trend of different version and observing the most download version in the past 90 days are straightforward and intuitive.

crates.io Unstacked Download Graph

Conclusion​

This is a great tool for us to gauge the adoption rate of our new releases and we highly encourage user upgrading to newer release that contains feature updates and bug fixes.

What's new in SeaQuery 0.27.0

Β· 7 min read
SeaQL Team
Ivan Krivosheev

πŸŽ‰ We are pleased to release SeaQuery 0.27.0! Here are some feature highlights 🌟:

Dependency Upgrade​

[#356] We have upgraded a major dependency:

  • Upgrade sqlx to 0.6.1

You might need to upgrade the corresponding dependency in your application as well.

Drivers support​

We have reworked the way drivers work in SeaQuery: priori to 0.27.0, users have to invoke the sea_query_driver_* macros. Now each driver sqlx, postgres & rusqlite has their own supporting crate, which integrates tightly with the corresponding libraries. Checkout our integration examples below for more details.

[#383] Deprecate sea-query-driver in favour of sea-query-binder

[#422] Rusqlite support is moved to sea-query-rusqlite

[#433] Postgres support is moved to sea-query-postgres

// before
sea_query::sea_query_driver_postgres!();
use sea_query_driver_postgres::{bind_query, bind_query_as};

let (sql, values) = Query::select()
.from(Character::Table)
.expr(Func::count(Expr::col(Character::Id)))
.build(PostgresQueryBuilder);

let row = bind_query(sqlx::query(&sql), &values)
.fetch_one(&mut pool)
.await
.unwrap();

// now
use sea_query_binder::SqlxBinder;

let (sql, values) = Query::select()
.from(Character::Table)
.expr(Func::count(Expr::col(Character::Id)))
.build_sqlx(PostgresQueryBuilder);

let row = sqlx::query_with(&sql, values)
.fetch_one(&mut pool)
.await
.unwrap();

// You can now make use of SQLx's `query_as_with` nicely:
let rows = sqlx::query_as_with::<_, StructWithFromRow, _>(&sql, values)
.fetch_all(&mut pool)
.await
.unwrap();

Support sub-query operators: EXISTS, ALL, ANY, SOME​

[#118] Added sub-query operators: EXISTS, ALL, ANY, SOME

let query = Query::select()
.column(Char::Id)
.from(Char::Table)
.and_where(
Expr::col(Char::Id)
.eq(
Expr::any(
Query::select().column(Char::Id).from(Char::Table).take()
)
)
)
.to_owned();

assert_eq!(
query.to_string(MysqlQueryBuilder),
r#"SELECT `id` FROM `character` WHERE `id` = ANY(SELECT `id` FROM `character`)"#
);
assert_eq!(
query.to_string(PostgresQueryBuilder),
r#"SELECT "id" FROM "character" WHERE "id" = ANY(SELECT "id" FROM "character")"#
);

Support ON CONFLICT WHERE​

[#366] Added support to ON CONFLICT WHERE

let query = Query::insert()
.into_table(Glyph::Table)
.columns([Glyph::Aspect, Glyph::Image])
.values_panic(vec![
2.into(),
3.into(),
])
.on_conflict(
OnConflict::column(Glyph::Id)
.update_expr((Glyph::Image, Expr::val(1).add(2)))
.target_and_where(Expr::tbl(Glyph::Table, Glyph::Aspect).is_null())
.to_owned()
)
.to_owned();

assert_eq!(
query.to_string(MysqlQueryBuilder),
r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (2, 3) ON DUPLICATE KEY UPDATE `image` = 1 + 2"#
);
assert_eq!(
query.to_string(PostgresQueryBuilder),
r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2, 3) ON CONFLICT ("id") WHERE "glyph"."aspect" IS NULL DO UPDATE SET "image" = 1 + 2"#
);
assert_eq!(
query.to_string(SqliteQueryBuilder),
r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2, 3) ON CONFLICT ("id") WHERE "glyph"."aspect" IS NULL DO UPDATE SET "image" = 1 + 2"#
);

Changed cond_where chaining semantics​

[#414] Changed cond_where chaining semantics

// Before: will extend current Condition
assert_eq!(
Query::select()
.cond_where(any![Expr::col(Glyph::Id).eq(1), Expr::col(Glyph::Id).eq(2)])
.cond_where(Expr::col(Glyph::Id).eq(3))
.to_owned()
.to_string(PostgresQueryBuilder),
r#"SELECT WHERE "id" = 1 OR "id" = 2 OR "id" = 3"#
);
// Before: confusing, since it depends on the order of invocation:
assert_eq!(
Query::select()
.cond_where(Expr::col(Glyph::Id).eq(3))
.cond_where(any![Expr::col(Glyph::Id).eq(1), Expr::col(Glyph::Id).eq(2)])
.to_owned()
.to_string(PostgresQueryBuilder),
r#"SELECT WHERE "id" = 3 AND ("id" = 1 OR "id" = 2)"#
);
// Now: will always conjoin with `AND`
assert_eq!(
Query::select()
.cond_where(Expr::col(Glyph::Id).eq(1))
.cond_where(any![Expr::col(Glyph::Id).eq(2), Expr::col(Glyph::Id).eq(3)])
.to_owned()
.to_string(PostgresQueryBuilder),
r#"SELECT WHERE "id" = 1 AND ("id" = 2 OR "id" = 3)"#
);
// Now: so they are now equivalent
assert_eq!(
Query::select()
.cond_where(any![Expr::col(Glyph::Id).eq(2), Expr::col(Glyph::Id).eq(3)])
.cond_where(Expr::col(Glyph::Id).eq(1))
.to_owned()
.to_string(PostgresQueryBuilder),
r#"SELECT WHERE ("id" = 2 OR "id" = 3) AND "id" = 1"#
);

Added OnConflict::value and OnConflict::values​

[#451] Implementation From<T> for any Into<Value> into SimpleExpr

// Before: notice the tuple
OnConflict::column(Glyph::Id).update_expr((Glyph::Image, Expr::val(1).add(2)))
// After: it accepts `Value` as well as `SimpleExpr`
OnConflict::column(Glyph::Id).value(Glyph::Image, Expr::val(1).add(2))

Improvement to ColumnDef::default​

[#347] ColumnDef::default now accepts Into<SimpleExpr> instead Into<Value>

// Now we can write:
ColumnDef::new(Char::FontId)
.timestamp()
.default(Expr::current_timestamp())

Breaking Changes​

  • [#386] Changed in_tuples interface to accept IntoValueTuple
  • [#320] Removed deprecated methods
  • [#440] CURRENT_TIMESTAMP changed from being a function to keyword
  • [#375] Update SQLite boolean type from integer to boolean`
  • [#451] Deprecated OnConflict::update_value, OnConflict::update_values, OnConflict::update_expr, OnConflict::update_exprs
  • [#451] Deprecated InsertStatement::exprs, InsertStatement::exprs_panic
  • [#451] Deprecated UpdateStatement::col_expr, UpdateStatement::value_expr, UpdateStatement::exprs
  • [#451] UpdateStatement::value now accept Into<SimpleExpr> instead of Into<Value>
  • [#451] Expr::case, CaseStatement::case and CaseStatement::finally now accepts Into<SimpleExpr> instead of Into<Expr>
  • [#460] InsertStatement::values, UpdateStatement::values now accepts IntoIterator<Item = SimpleExpr> instead of IntoIterator<Item = Value>
  • [#409] Use native api from SQLx for SQLite to work with time
  • [#435] Changed type of ColumnType::Enum from (String, Vec<String>) to Enum { name: DynIden, variants: Vec<DynIden>}

Miscellaneous Enhancements​

  • [#336] Added support one dimension Postgres array for SQLx
  • [#373] Support CROSS JOIN
  • [#457] Added support DROP COLUMN for SQLite
  • [#466] Added YEAR, BIT and VARBIT types
  • [#338] Handle Postgres schema name for schema statements
  • [#418] Added %, << and >> binary operators
  • [#329] Added RAND function
  • [#425] Implements Display for Value
  • [#427] Added INTERSECT and EXCEPT to UnionType
  • [#448] OrderedStatement::order_by_customs, OrderedStatement::order_by_columns, OverStatement::partition_by_customs, OverStatement::partition_by_columns now accepts IntoIterator<Item = T> instead of Vec<T>
  • [#452] TableAlterStatement::rename_column, TableAlterStatement::drop_column, ColumnDef::new, ColumnDef::new_with_type now accepts IntoIden instead of Iden
  • [#426] Cleanup IndexBuilder trait methods
  • [#436] Introduce SqlWriter trait
  • [#448] Remove unneeded vec! from examples

Bug Fixes​

  • [#449] distinct_on properly handles ColumnRef
  • [#461] Removed ON for DROP INDEX for SQLite
  • [#468] Change datetime string format to include microseconds
  • [#452] ALTER TABLE for PosgreSQL with UNIQUE constraint

Integration Examples​

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

Community​

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

Getting Started with Seaography

Β· 6 min read
SeaQL Team
Chris Tsang

Seaography is a GraphQL framework for building GraphQL resolvers using SeaORM. It ships with a CLI tool that can generate ready-to-compile Rust projects from existing MySQL, Postgres and SQLite databases.

The design and implementation of Seaography can be found on our release blog post and documentation.

Extending a SeaORM project​

Since Seaography is built on top of SeaORM, you can easily build a GraphQL server from a SeaORM project.

Start by adding Seaography and GraphQL dependencies to your Cargo.toml.

[dependencies]
sea-orm = { version = "^0.9", features = [ ... ] }
+ seaography = { version = "^0.1", features = [ "with-decimal", "with-chrono" ] }
+ async-graphql = { version = "4.0.10", features = ["decimal", "chrono", "dataloader"] }
+ async-graphql-poem = { version = "4.0.10" }

Then, derive a few macros on the SeaORM entities.

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

#[derive(
Clone,
Debug,
PartialEq,
DeriveEntityModel,
+ async_graphql::SimpleObject,
+ seaography::macros::Filter,
)]
+ #[graphql(complex)]
+ #[graphql(name = "FilmActor")]
#[sea_orm(table_name = "film_actor")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub actor_id: i32,
#[sea_orm(primary_key, auto_increment = false)]
pub film_id: i32,
pub last_update: DateTimeUtc,
}

#[derive(
Copy,
Clone,
Debug,
EnumIter,
DeriveRelation,
+ seaography::macros::RelationsCompact,
)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::film::Entity",
from = "Column::FilmId",
to = "super::film::Column::FilmId",
on_update = "Cascade",
on_delete = "NoAction"
)]
Film,
#[sea_orm(
belongs_to = "super::actor::Entity",
from = "Column::ActorId",
to = "super::actor::Column::ActorId",
on_update = "Cascade",
on_delete = "NoAction"
)]
Actor,
}

We also need to define QueryRoot for the GraphQL server. This define the GraphQL schema.

#[derive(Debug, seaography::macros::QueryRoot)]
#[seaography(entity = "crate::entities::actor")]
#[seaography(entity = "crate::entities::film")]
#[seaography(entity = "crate::entities::film_actor")]
pub struct QueryRoot;
use sea_orm::prelude::*;

pub mod entities;
pub mod query_root;

pub use query_root::QueryRoot;

pub struct OrmDataloader {
pub db: DatabaseConnection,
}

Finally, create an executable to drive the GraphQL server.

use async_graphql::{
dataloader::DataLoader,
http::{playground_source, GraphQLPlaygroundConfig},
EmptyMutation, EmptySubscription, Schema,
};
use async_graphql_poem::GraphQL;
use poem::{handler, listener::TcpListener, web::Html, IntoResponse, Route, Server};
use sea_orm::Database;
use seaography_example_project::*;
// ...

#[handler]
async fn graphql_playground() -> impl IntoResponse {
Html(playground_source(GraphQLPlaygroundConfig::new("/")))
}

#[tokio::main]
async fn main() {
// ...

let database = Database::connect(db_url).await.unwrap();
let orm_dataloader: DataLoader<OrmDataloader> = DataLoader::new(
OrmDataloader { db: database.clone() },
tokio::spawn,
);

let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription)
.data(database)
.data(orm_dataloader)
.finish();

let app = Route::new()
.at("/", get(graphql_playground)
.post(GraphQL::new(schema)));

Server::new(TcpListener::bind("0.0.0.0:8000"))
.run(app)
.await
.unwrap();
}

Generating a project from database​

If all you have is a database schema, good news! You can setup a GraphQL server without writing a single line of code.

Install seaography-cli, it helps you generate SeaORM entities along with a full Rust project based on a database schema.

cargo install seaography-cli

Run seaography-cli to generate code for the GraphQL server.

# The command take three arguments
seaography-cli <DATABASE_URL> <CRATE_NAME> <DESTINATION>

# MySQL
seaography-cli mysql://root:root@localhost/sakila seaography-mysql-example examples/mysql
# PostgreSQL
seaography-cli postgres://root:root@localhost/sakila seaography-postgres-example examples/postgres
# SQLite
seaography-cli sqlite://examples/sqlite/sakila.db seaography-sqlite-example examples/sqliteql

Checkout the example projects​

We have the following examples for you, alongside with the SQL scripts to initialize the database.

All examples provide a web-based GraphQL playground when running, so you can inspect the GraphQL schema and make queries. We also hosted a demo GraphQL playground in case you can't wait to play with it.

Starting the GraphQL Server​

Your GraphQL server is ready to launch! Go to the Rust project root then execute cargo run to spin it up.

$ cargo run

Playground: http://localhost:8000

Visit the GraphQL playground at http://localhost:8000

GraphQL Playground

Query Data via GraphQL​

Let say we want to get the first 3 films released on or after year 2006 sorted in ascending order of its title.

{
film(
pagination: { limit: 3, page: 0 }
filters: { releaseYear: { gte: "2006" } }
orderBy: { title: ASC }
) {
data {
filmId
title
description
releaseYear
filmActor {
actor {
actorId
firstName
lastName
}
}
}
pages
current
}
}

We got the following JSON result after running the GraphQL query.

{
"data": {
"film": {
"data": [
{
"filmId": 1,
"title": "ACADEMY DINOSAUR",
"description": "An Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies",
"releaseYear": "2006",
"filmActor": [
{
"actor": {
"actorId": 1,
"firstName": "PENELOPE",
"lastName": "GUINESS"
}
},
{
"actor": {
"actorId": 10,
"firstName": "CHRISTIAN",
"lastName": "GABLE"
}
},
// ...
]
},
{
"filmId": 2,
"title": "ACE GOLDFINGER",
"description": "A Astounding Epistle of a Database Administrator And a Explorer who must Find a Car in Ancient China",
"releaseYear": "2006",
"filmActor": [
// ...
]
},
// ...
],
"pages": 334,
"current": 0
}
}
}

Behind the scene, the following SQL were queried:

SELECT "film"."film_id",
"film"."title",
"film"."description",
"film"."release_year",
"film"."language_id",
"film"."original_language_id",
"film"."rental_duration",
"film"."rental_rate",
"film"."length",
"film"."replacement_cost",
"film"."rating",
"film"."special_features",
"film"."last_update"
FROM "film"
WHERE "film"."release_year" >= '2006'
ORDER BY "film"."title" ASC
LIMIT 3 OFFSET 0

SELECT "film_actor"."actor_id", "film_actor"."film_id", "film_actor"."last_update"
FROM "film_actor"
WHERE "film_actor"."film_id" IN (1, 3, 2)

SELECT "actor"."actor_id", "actor"."first_name", "actor"."last_name", "actor"."last_update"
FROM "actor"
WHERE "actor"."actor_id" IN (24, 162, 20, 160, 1, 188, 123, 30, 53, 40, 2, 64, 85, 198, 10, 19, 108, 90)

Under the hood, Seaography uses async_graphql::dataloader in querying nested objects to tackle the N+1 problem.

To learn more, checkout the Seaography Documentation.

Conclusion​

Seaography is an ergonomic library that turns SeaORM entities into GraphQL nodes. It provides a set of utilities and combined with a code generator makes GraphQL API building a breeze.

However, Seaography is still a new-born. Like all other open-source projects developed by passionate Rust developers, you can contribute to it if you also find the concept interesting. With its addition to the SeaQL ecosystem, we are one step closer to the vision of Rust being the best tool for data engineering.

People​

Seaography is created by:

Panagiotis Karatakis

Summer of Code Contributor; developer of Seaography

Chris Tsang

Summer of Code Mentor; lead developer of SeaQL

Billy Chan

Summer of Code Mentor; core member of SeaQL

Introducing Seaography 🧭

Β· 5 min read
SeaQL Team
Chris Tsang

What a fruitful Summer of Code! Today, we are excited to introduce Seaography to the SeaQL community. Seaography is a GraphQL framework for building GraphQL resolvers using SeaORM. It ships with a CLI tool that can generate ready-to-compile Rust projects from existing MySQL, Postgres and SQLite databases.

Motivation​

We observed that other ecosystems have similar tools such as PostGraphile and Hasura allowing users to query a database via GraphQL with minimal effort upfront. We decided to bring that seamless experience to the Rust ecosystem.

For existing SeaORM users, adding a GraphQL API is straight forward. Start by adding seaography and async-graphql dependencies to your crate. Then, deriving a few extra derive macros to the SeaORM entities. Finally, spin up a GraphQL server to serve queries!

If you are new to SeaORM, no worries, we have your back. You only need to provide a database connection, and seaography-cli will generate the SeaORM entities together with a complete Rust project!

Design​

We considered two approaches in our initial discussion: 1) blackbox query engine 2) code generator. The drawback with a blackbox query engine is it's difficult to customize or extend its behaviour, making it difficult to develop and operate in the long run. We opted the code generator approach, giving users full control and endless possibilities with the versatile async Rust ecosystem.

This project is separated into the following crates:

  • seaography: The facade crate; exporting macros, structures and helper functions to turn SeaORM entities into GraphQL nodes.

  • seaography-cli: The CLI tool; it generates SeaORM entities along with a full Rust project based on a user-provided database.

  • seaography-discoverer: A helper crate used by the CLI tool to discover the database schema and transform into a generic format.

  • seaography-generator: A helper crate used by the CLI tool to consume the database schema and generate a full Rust project.

  • seaography-derive: A set of procedural macros to derive types and trait implementations on SeaORM entities, turning them into GraphQL nodes.

Features​

  • Relational query (1-to-1, 1-to-N)
  • Pagination on query's root entity
  • Filter with operators (e.g. gt, lt, eq)
  • Order by any column

Getting Started​

To quick start, we have the following examples for you, alongside with the SQL scripts to initialize the database.

All examples provide a web-based GraphQL playground when running, so you can inspect the GraphQL schema and make queries. We also hosted a demo GraphQL playground in case you can't wait to play with it.

For more documentation, visit www.sea-ql.org/Seaography.

What's Next?​

This project passed the first milestone shipping the essential features, but it still has a long way to go. The next milestone would be:

  • Query enhancements
    • Filter related queries
    • Filter based on related queries properties
    • Paginate related queries
    • Order by related queries
  • Cursor based pagination
  • Single entity query
  • Mutations
    • Insert single entity
    • Insert batch entities
    • Update single entity
    • Update batch entities using filter
    • Delete single entity
    • Delete batch entities

Conclusion​

Seaography is an ergonomic library that turns SeaORM entities into GraphQL nodes. It provides a set of utilities and combined with a code generator makes GraphQL API building a breeze.

However, Seaography is still a new-born. Like all other open-source projects developed by passionate Rust developers, you can contribute to it if you also find the concept interesting. With its addition to the SeaQL ecosystem, we are one step closer to the vision of Rust being the best tool for data engineering.

People​

Seaography is created by:

Panagiotis Karatakis

Summer of Code Contributor; developer of Seaography

Chris Tsang

Summer of Code Mentor; lead developer of SeaQL

Billy Chan

Summer of Code Mentor; core member of SeaQL

Celebrating 3,000+ GitHub Stars πŸŽ‰

Β· 8 min read
SeaQL Team
Chris Tsang

We are celebrating the milestone of reaching 3,000 GitHub stars across all SeaQL repositories!

This wouldn't have happened without your support and contribution, so we want to thank the community for being with us along the way.

The Journey​

SeaQL.org was founded back in 2020. We devoted ourselves into developing open source libraries that help Rust developers to build data intensive applications. In the past two years, we published and maintained four open source libraries: SeaQuery, SeaSchema, SeaORM and StarfishQL. Each library is designed to fill a niche in the Rust ecosystem, and they are made to play well with other Rust libraries.

2020​

  • Oct 2020: SeaQL founded
  • Dec 2020: SeaQuery first released

2021​

  • Apr 2021: SeaSchema first released
  • Aug 2021: SeaORM first released
  • Nov 2021: SeaORM reached 0.4.0
  • Dec 2021: SeaQuery reached 0.20.0
  • Dec 2021: SeaSchema reached 0.4.0

2022​

  • Apr 2022: SeaQL selected as a Google Summer of Code 2022 mentor organization
  • Apr 2022: StarfishQL first released
  • Jul 2022: SeaQuery reached 0.26.2
  • Jul 2022: SeaSchema reached 0.9.3
  • Jul 2022: SeaORM reached 0.9.1
  • Aug 2022: SeaQL reached 3,000+ GitHub stars

Where're We Now?​

We're pleased by the adoption by the Rust community. We couldn't make it this far without your feedback and contributions.

4 πŸ“¦

Open source projects

5 🏬

Startups using SeaQL

1,972 🎈

Dependent projects

131 πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦

Contributors

1,061 βœ…

Merged PRs & resolved issues

3,158 ⭐

GitHub stars

432 πŸ—£οΈ

Discord members

87,937 ⌨️

Lines of Rust

667,769 πŸ’Ώ

Downloads on crates.io

* as of Aug 12

Core Members​

Our team has grown from two people initially into four. We always welcome passionate engineers to join us!

Chris Tsang

Founder. Led the initial development and maintaining the projects.

Billy Chan

Founding member. Contributed many features and bug fixes. Keeps the community alive.

Ivan Krivosheev

Joined in 2022. Contributed many features and bug fixes, most notably to SeaQuery.

Sanford Pun

Developed StarfishQL and wrote SeaORM's tutorial.

Special Thanks​

Marco Napetti

Contributed transaction, streaming and tracing API to SeaORM.

nitnelave

Contributed binder crate and other improvements to SeaQuery.

Sam Samai

Developed SeaORM's test suite and demo schema.

Daniel Lyne

Developed SeaSchema's Postgres implementation.

Charles Chege

Developed SeaSchema's SQLite implementation.

Sponsors​

If you are feeling generous, a small donation will be greatly appreciated.

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

Γ‰mile Fugulin

Dean Sheather

Shane Sveller

Sakti Dwi Cahyono

Unnamed Sponsor

Unnamed Sponsor

Contributors​

Many features and enhancements are actually proposed and implemented by the community. We want to take this chance to thank all our contributors!

What's Next?​

We have two ongoing Summer of Code 2022 projects to enrich the SeaQL ecosystem, planning to be released later this year. In the meantime, we're focusing on improving existing SeaQL libraries until reaching version 1.0, we'd love to hear comments and feedback from the community.

If you like what we do, consider starring, commenting, sharing, contributing and together building for Rust's future!

What's new in SeaQuery 0.26.0

Β· 4 min read
SeaQL Team
Ivan Krivosheev

πŸŽ‰ We are pleased to release SeaQuery 0.26.0! Here are some feature highlights 🌟:

Dependency Upgrades​

[#356] We have upgraded a few major dependencies:

Note that you might need to upgrade the corresponding dependency on your application as well.

VALUES lists​

[#351] Add support for VALUES lists

// SELECT * FROM (VALUES (1, 'hello'), (2, 'world')) AS "x"
let query = SelectStatement::new()
.expr(Expr::asterisk())
.from_values(vec![(1i32, "hello"), (2, "world")], Alias::new("x"))
.to_owned();

assert_eq!(
query.to_string(PostgresQueryBuilder),
r#"SELECT * FROM (VALUES (1, 'hello'), (2, 'world')) AS "x""#
);

Introduce sea-query-binder​

[#273] Native support SQLx without marcos

use sea_query_binder::SqlxBinder;

// Create SeaQuery query with prepare SQLx
let (sql, values) = Query::select()
.columns([
Character::Id,
Character::Uuid,
Character::Character,
Character::FontSize,
Character::Meta,
Character::Decimal,
Character::BigDecimal,
Character::Created,
Character::Inet,
Character::MacAddress,
])
.from(Character::Table)
.order_by(Character::Id, Order::Desc)
.build_sqlx(PostgresQueryBuilder);

// Execute query
let rows = sqlx::query_as_with::<_, CharacterStructChrono, _>(&sql, values)
.fetch_all(&mut pool)
.await?;

// Print rows
for row in rows.iter() {
println!("{:?}", row);
}

CASE WHEN statement support​

[#304] Add support for CASE WHEN statement

let query = Query::select()
.expr_as(
CaseStatement::new()
.case(Expr::tbl(Glyph::Table, Glyph::Aspect).is_in(vec![2, 4]), Expr::val(true))
.finally(Expr::val(false)),
Alias::new("is_even")
)
.from(Glyph::Table)
.to_owned();

assert_eq!(
query.to_string(PostgresQueryBuilder),
r#"SELECT (CASE WHEN ("glyph"."aspect" IN (2, 4)) THEN TRUE ELSE FALSE END) AS "is_even" FROM "glyph""#
);

Add support for Ip(4,6)Network and MacAddress​

[#309] Add support for Network types in PostgreSQL backend

Introduce sea-query-attr​

[#296] Proc-macro for deriving Iden enum from struct

use sea_query::gen_type_def;

#[gen_type_def]
pub struct Hello {
pub name: String
}

println!("{:?}", HelloTypeDef::Name);

Add ability to alter foreign keys​

[#299] Add support for ALTER foreign Keys

let foreign_key_char = TableForeignKey::new()
.name("FK_character_glyph")
.from_tbl(Char::Table)
.from_col(Char::FontId)
.from_col(Char::Id)
.to_tbl(Glyph::Table)
.to_col(Char::FontId)
.to_col(Char::Id)
.to_owned();

let table = Table::alter()
.table(Character::Table)
.add_foreign_key(&foreign_key_char)
.to_owned();

assert_eq!(
table.to_string(PostgresQueryBuilder),
vec![
r#"ALTER TABLE "character""#,
r#"ADD CONSTRAINT "FK_character_glyph""#,
r#"FOREIGN KEY ("font_id", "id") REFERENCES "glyph" ("font_id", "id")"#,
r#"ON DELETE CASCADE ON UPDATE CASCADE,"#,
]
.join(" ")
);

Select DISTINCT ON​

[#250]

let query = Query::select()
.from(Char::Table)
.distinct_on(vec![Char::Character])
.column(Char::Character)
.column(Char::SizeW)
.column(Char::SizeH)
.to_owned();

assert_eq!(
query.to_string(PostgresQueryBuilder),
r#"SELECT DISTINCT ON ("character") "character", "size_w", "size_h" FROM "character""#
);

Miscellaneous Enhancements​

  • [#353] Support LIKE ... ESCAPE ... expression
  • [#306] Move escape and unescape string to backend
  • [#365] Add method to make a column nullable
  • [#348] Add is & is_not to Expr
  • [#349] Add CURRENT_TIMESTAMP function
  • [#345] Add in_tuple method to Expr
  • [#266] Insert Default
  • [#324] Make sea-query-driver an optional dependency
  • [#334] Add ABS function
  • [#332] Support IF NOT EXISTS when create index
  • [#314] Support different blob types in MySQL
  • [#331] Add VarBinary column type
  • [#335] RETURNING expression supporting SimpleExpr

Integration Examples​

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

Community​

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

Engineering at SeaQL.org

Β· 5 min read
Chris Tsang
Lead Developer

It's hard to pin down the exact date, but I think SeaQL.org was setup in July 2020, a little over a year ago. Over the course of the year, SeaORM went from 0.1 to 0.9 and the number of users kept growing. I would like to outline our engineering process in this blog post, and perhaps it can serve as a reference or guidance to prospective contributors and the future maintainer of this project.

In the open source world, the Benevolent Dictator for Life (BDL) model underpins a number of successful open source projects. That's not me! As a maintainer, I believe in an open, bottom-up, iterative and progressive approach. Let me explain each of these words and what they mean to me.

Open​

Open as in source availability, but also engineering. We always welcome new contributors! We'd openly discuss ideas and designs. I would often explain why a decision was made in the first place for various things. The project is structured not as a monorepo, but several interdependent repos. This reduces the friction for new contributors, because they can have a smaller field of vision to focus on solving one particular problem at hand.

Bottom-up​

We rely on users to file feature requests, bug reports and of course pull requests to drive the project forward. The great thing is, for every feature / bug fix, there is a use case for it and a confirmation from a real user that it works and is reasonable. As a maintainer, I could not have first hand experience for all features and so could not understand some of the pain points.

Iterative​

Open source software is imperfect, impermanent and incomplete. While I do have a grand vision in mind, we do not try rushing it all the way in one charge, nor keeping a project secret until it is 'complete'. Good old 'release early, release often' - we would release an initial working version of a tool, gather user feedback and improve upon it, often reimplementing a few things and break a few others - which brings us to the next point.

Progressive​

Favour progression. Always look forward and leave legacy behind. It does not mean that we would arbitrary break things, but when a decision is made, we'd always imagine how the software should be without historic context. We'd provide migrate paths and encourage users to move forward with us. After all, Rust is a young and evolving language! You may or may not know that async was just stabilized in 2020.

Enough said for the philosophy, let's now talk about the actual engineering process.

1. Idea & Design​

We first have some vague idea on what problem we want to tackle. As we put in more details to the use case, we can define the problem and brainstorm solutions. Then we look for workable ways to implement that in Rust.

2. Implementation​

An initial proof of concept is appreciated. We iterate on the implementation to reduce the impact and improve the maintainability.

3. Testing​

We rely on automated tests. Every feature should come with corresponding tests, and a release is good if and only if all tests are green. Which means for features not covered by our test suite, it is an uncertainty to when we would break them. So if certain undocumented feature is important to you, we encourage you to add that to our test suite.

4. Documentation​

Coding is not complete without documentation. Rust doc tests kill two birds with one stone and so is greatly appreciated. For SeaORM we have separate documentation repository and tutorial repository. It takes a lot of effort to maintain those to be up to date, and right now it's mostly done by our core contributors.

5. Release​

We run on a release train model, although the frequency varies. The ethos is to have small number breaking changes often. At one point, SeaQuery has a new release every week. SeaORM runs on monthly, although it more or less relaxes to bimonthly now. At any time, we maintain two branches, the latest release and master. PRs are always merged into master, and if it is non-breaking (and worthy) I would backport it to the release branch and make a minor release. At the end, I want to maintain momentum and move forward together with the community. Users can have a rough expectation on when merges will be released. And there are just lots of change we cannot avoid a breaking release as of the current state of the Rust ecosystem. Users are advised to upgrade regularly, and we ship along many small improvements to encourage that.

Conclusion​

Open source software is a collaborative effort and thank you all who participated! Also a big thanks to SeaQL's core contributors who made wonders. If you have not already, I invite you to star all our repositories. If you want to support us materially, a small donation would make a big difference. SeaQL the organization is still in its infancy, and your support is vital to SeaQL's longevity and the prospect of the Rust community.

What's new in SeaORM 0.9.0

Β· 16 min read
SeaQL Team
Chris Tsang

πŸŽ‰ We are pleased to release SeaORM 0.9.0 today! Here are some feature highlights 🌟:

Dependency Upgrades​

[#834] We have upgraded a few major dependencies:

Note that you might need to upgrade the corresponding dependency on your application as well.

Proposed by:

Rob Gilson

boraarslan

Contributed by:

Billy Chan

Cursor Pagination​

[#822] Paginate models based on column(s) such as the primary key.

// Create a cursor that order by `cake`.`id`
let mut cursor = cake::Entity::find().cursor_by(cake::Column::Id);

// Filter paginated result by `cake`.`id` > 1 AND `cake`.`id` < 100
cursor.after(1).before(100);

// Get first 10 rows (order by `cake`.`id` ASC)
let rows: Vec<cake::Model> = cursor.first(10).all(db).await?;

// Get last 10 rows (order by `cake`.`id` DESC but rows are returned in ascending order)
let rows: Vec<cake::Model> = cursor.last(10).all(db).await?;

Proposed by:

Lucas Berezy

Contributed by:

Γ‰mile Fugulin

Billy Chan

Insert On Conflict​

[#791] Insert an active model with on conflict behaviour.

let orange = cake::ActiveModel {
id: ActiveValue::set(2),
name: ActiveValue::set("Orange".to_owned()),
};

// On conflict do nothing:
// - INSERT INTO "cake" ("id", "name") VALUES (2, 'Orange') ON CONFLICT ("name") DO NOTHING
cake::Entity::insert(orange.clone())
.on_conflict(
sea_query::OnConflict::column(cake::Column::Name)
.do_nothing()
.to_owned()
)
.exec(db)
.await?;

// On conflict do update:
// - INSERT INTO "cake" ("id", "name") VALUES (2, 'Orange') ON CONFLICT ("name") DO UPDATE SET "name" = "excluded"."name"
cake::Entity::insert(orange)
.on_conflict(
sea_query::OnConflict::column(cake::Column::Name)
.update_column(cake::Column::Name)
.to_owned()
)
.exec(db)
.await?;

Proposed by:

baoyachi. Aka Rust Hairy crabs

Contributed by:

liberwang1013

Join Table with Custom Conditions and Table Alias​

[#793, #852] Click Custom Join Conditions and Custom Joins to learn more.

assert_eq!(
cake::Entity::find()
.column_as(
Expr::tbl(Alias::new("fruit_alias"), fruit::Column::Name).into_simple_expr(),
"fruit_name"
)
.join_as(
JoinType::LeftJoin,
cake::Relation::Fruit
.def()
.on_condition(|_left, right| {
Expr::tbl(right, fruit::Column::Name)
.like("%tropical%")
.into_condition()
}),
Alias::new("fruit_alias")
)
.build(DbBackend::MySql)
.to_string(),
[
"SELECT `cake`.`id`, `cake`.`name`, `fruit_alias`.`name` AS `fruit_name` FROM `cake`",
"LEFT JOIN `fruit` AS `fruit_alias` ON `cake`.`id` = `fruit_alias`.`cake_id` AND `fruit_alias`.`name` LIKE '%tropical%'",
]
.join(" ")
);

Proposed by:

Chris Tsang

Tuetuopay

LoΓ―c

Contributed by:

Billy Chan

Matt

liberwang1013

(de)serialize Custom JSON Type​

[#794] JSON stored in the database could be deserialized into custom struct in Rust.

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "json_struct")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
// JSON column defined in `serde_json::Value`
pub json: Json,
// JSON column defined in custom struct
pub json_value: KeyValue,
pub json_value_opt: Option<KeyValue>,
}

// The custom struct must derive `FromJsonQueryResult`, `Serialize` and `Deserialize`
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, FromJsonQueryResult)]
pub struct KeyValue {
pub id: i32,
pub name: String,
pub price: f32,
pub notes: Option<String>,
}

Proposed by:

Mara Schulke

Chris Tsang

Contributed by:

Billy Chan

Derived Migration Name​

[#736] Introduce DeriveMigrationName procedural macros to infer migration name from the file name.

use sea_orm_migration::prelude::*;

// Used to be...
pub struct Migration;

impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220120_000001_create_post_table"
}
}

// Now... derive `DeriveMigrationName`,
// no longer have to specify the migration name explicitly
#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table( ... )
.await
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table( ... )
.await
}
}

Proposed by:

Chris Tsang

Contributed by:

smonv

Lukas Potthast

Billy Chan

SeaORM CLI Improvements​

  • [#735] Improve logging of generate entity command
  • [#588] Generate enum with numeric like variants
  • [#755] Allow old pending migration to be applied
  • [#837] Skip generating entity for ignored tables
  • [#724] Generate code for time crate
  • [#850] Add various blob column types
  • [#422] Generate entity files with Postgres's schema name
  • [#851] Skip checking connection string for credentials

Proposed & Contributed by:

ttys3

kyoto7250

yb3616

Γ‰mile Fugulin

Bastian

Nahua

Mike

Frank Horvath

Maikel Wever

Miscellaneous Enhancements​

  • [#800] Added sqlx_logging_level to ConnectOptions
  • [#768] Added num_items_and_pages to Paginator
  • [#849] Added TryFromU64 for time
  • [#853] Include column name in TryGetError::Null
  • [#778] Refactor stream metrics

Proposed & Contributed by:

SandaruKasa

Eric

Γ‰mile Fugulin

Renato Dinhani

kyoto7250

Marco Napetti

Integration Examples​

SeaORM plays well with the other crates in the async ecosystem. We maintain an array of example projects for building REST, GraphQL and gRPC services. More examples wanted!

Our GitHub Sponsor profile is up! If you feel generous, a small donation will be greatly appreciated.

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

Γ‰mile Fugulin

Dean Sheather

Shane Sveller

Sakti Dwi Cahyono

Unnamed Sponsor

Unnamed Sponsor

Community​

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

Here is the roadmap for SeaORM 0.10.x.