Giter Site home page Giter Site logo

seaql / sea-orm Goto Github PK

View Code? Open in Web Editor NEW
6.2K 33.0 443.0 9.12 MB

๐Ÿš An async & dynamic ORM for Rust

Home Page: https://www.sea-ql.org/SeaORM/

License: Apache License 2.0

Rust 99.65% Shell 0.35%
mysql rust sqlite orm postgres tokio async-std actix database sql

sea-orm's Introduction

SeaORM

๐Ÿš An async & dynamic ORM for Rust

crate docs build status

SeaORM

SeaORM is a relational ORM to help you build web services in Rust with the familiarity of dynamic languages.

GitHub stars If you like what we do, consider starring, sharing and contributing!

Please help us with maintaining SeaORM by completing the SeaQL Community Survey 2024!

Getting Started

Integration examples:

Support

Discord Join our Discord server to chat with other members of the SeaQL community!

Professional support on Rust programming and best practices is available. You can email us for a quote!

Features

  1. Async

    Relying on SQLx, SeaORM is a new library with async support from day 1.

  2. Dynamic

    Built upon SeaQuery, SeaORM allows you to build complex dynamic queries.

  3. Testable

    Use mock connections and/or SQLite to write tests for your application logic.

  4. Service Oriented

    Quickly build services that join, filter, sort and paginate data in REST, GraphQL and gRPC APIs.

A quick taste of SeaORM

Entity

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

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

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

impl Related<super::fruit::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::Fruit.def()
    }
}

Select

// find all models
let cakes: Vec<cake::Model> = Cake::find().all(db).await?;

// find and filter
let chocolate: Vec<cake::Model> = Cake::find()
    .filter(cake::Column::Name.contains("chocolate"))
    .all(db)
    .await?;

// find one model
let cheese: Option<cake::Model> = Cake::find_by_id(1).one(db).await?;
let cheese: cake::Model = cheese.unwrap();

// find related models (lazy)
let fruits: Vec<fruit::Model> = cheese.find_related(Fruit).all(db).await?;

// find related models (eager)
let cake_with_fruits: Vec<(cake::Model, Vec<fruit::Model>)> =
    Cake::find().find_with_related(Fruit).all(db).await?;

Insert

let apple = fruit::ActiveModel {
    name: Set("Apple".to_owned()),
    ..Default::default() // no need to set primary key
};

let pear = fruit::ActiveModel {
    name: Set("Pear".to_owned()),
    ..Default::default()
};

// insert one
let pear = pear.insert(db).await?;

// insert many
Fruit::insert_many([apple, pear]).exec(db).await?;

Update

use sea_orm::sea_query::{Expr, Value};

let pear: Option<fruit::Model> = Fruit::find_by_id(1).one(db).await?;
let mut pear: fruit::ActiveModel = pear.unwrap().into();

pear.name = Set("Sweet pear".to_owned());

// update one
let pear: fruit::Model = pear.update(db).await?;

// update many: UPDATE "fruit" SET "cake_id" = NULL WHERE "fruit"."name" LIKE '%Apple%'
Fruit::update_many()
    .col_expr(fruit::Column::CakeId, Expr::value(Value::Int(None)))
    .filter(fruit::Column::Name.contains("Apple"))
    .exec(db)
    .await?;

Save

let banana = fruit::ActiveModel {
    id: NotSet,
    name: Set("Banana".to_owned()),
    ..Default::default()
};

// create, because primary key `id` is `NotSet`
let mut banana = banana.save(db).await?;

banana.name = Set("Banana Mongo".to_owned());

// update, because primary key `id` is `Set`
let banana = banana.save(db).await?;

Delete

// delete one
let orange: Option<fruit::Model> = Fruit::find_by_id(1).one(db).await?;
let orange: fruit::Model = orange.unwrap();
fruit::Entity::delete(orange.into_active_model())
    .exec(db)
    .await?;

// or simply
let orange: Option<fruit::Model> = Fruit::find_by_id(1).one(db).await?;
let orange: fruit::Model = orange.unwrap();
orange.delete(db).await?;

// delete many: DELETE FROM "fruit" WHERE "fruit"."name" LIKE 'Orange'
fruit::Entity::delete_many()
    .filter(fruit::Column::Name.contains("Orange"))
    .exec(db)
    .await?;

๐Ÿงญ Seaography: GraphQL integration (preview)

Seaography is a GraphQL framework built on top of SeaORM. Seaography allows you to build GraphQL resolvers quickly. With just a few commands, you can launch a GraphQL server from SeaORM entities!

Starting 0.12, seaography integration is built into sea-orm. While Seaography development is still in an early stage, it is especially useful in prototyping and building internal-use admin panels.

Look at the Seaography Example to learn more.

Learn More

  1. Design
  2. Architecture
  3. Engineering
  4. Change Log

Who's using SeaORM?

See Built with SeaORM. Feel free to submit yours!

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

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

A big shout out to our contributors!

Contributors

Sponsorship

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

We invite you to participate, contribute and together help build Rust's future.

Gold Sponsors

Mascot

A friend of Ferris, Terres the hermit crab is the official mascot of SeaORM. His hobby is collecting shells.

Terres

sea-orm's People

Contributors

angelonfira avatar anshap1719 avatar baoyachi avatar billy1624 avatar charleschege avatar chris-cantor avatar darkmmon avatar elbart avatar expurple avatar goodjooy avatar higherorderlogic avatar ikrivosheev avatar jimmycuadra avatar karatakis avatar kyoto7250 avatar lz1998 avatar mohs8421 avatar muhannadalrusayni avatar nappa85 avatar samsamai avatar sebpuetz avatar sergeiivankov avatar sytten avatar tasn avatar tqwewe avatar tsirysndr avatar ttys3 avatar tyt2y3 avatar wyatt-herkamp avatar yoshierahuang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sea-orm's Issues

Support of subquery

Just thinking where we need subquery support in sea-orm

  • select expression: SELECT (SELECT ...) AS subquery FROM ...
  • conditional expression: ... WHERE id IN (SELECT ...) ...
  • from expression: ... FROM (SELECT ...) ...
  • join expression: ... LEFT JOIN (SELECT ...) ON ...

How many kinds of subquery we want to support at this stage? I think supporting "select expression" and "conditional expression" are enough for now.

`QueryHelper::filter` should take a `ConditionWhere` as argument

Right now, filter takes a simpleExpr as argument, which can be combined into more complex boolean statements with .and and .or, but I think it would be a better interface (and clearer, and more consistent) if it took a sea_query::query::condition::ConditionWhere (or rather an Into<ConditionWhere>).

0.1.0 To Do Tasks

  1. Connection

    • Connection to Live Database
      • Database: abstract interface of a database
      • Connection: a live connection able to execute SQL statements
      • ExecutionResult: execution result of non select query
      • #83
    • Backend support
      • SQLx MySql
      • SQLx Postgres (#19)
      • #33
    • Mock Connection
      • Provide a mock database API for unit testing
  2. ORM Core

    • Components
      • Entity: encapsulates all the necessary specification of the corresponding database table
        • Column: column name
        • PrimaryKey: primary key name
        • Relation: definition of relation between tables
      • Model: struct to store the data of a database row
    • Query Result
      • serialize to serde_json::Value (#4)
    • Entity Generator
      • Given a table schema (from sea-schema), generate Entity file as Rust code (#11)
  3. Select from Table

    • Execute the SelectStatment and return one or many models
    • Select into custom (ad hoc) models
    • where (filter and condition)
    • order_by
    • Paginate selection result
      • Stream sync item (#5)
        i.e. into impl Stream<Item = Result<_, _>>
      • Test cases for Paginator implemented with mock connection
    • Relations: one to one and one to many
    • Relation: many to many via junction table
  4. Insert into Table

    • Insert one model into database
    • Insert multiple models as a single query
  5. Update from Table

    • Update a whole model to a row
    • ActiveModel: wrap Model and indicates which fields are modified, and only update respective columns
    • save semantics: insert if model is new, otherwise update
    • Update many using filters
  6. Delete from Table

    • Delete one model
    • Delete many models using filters (similar to select)
  7. Utility (#24)

    • Discovery: generate command to generate entities from existing database
  8. Test suite

    • bakery: a database schema for demonstration (#32)
    • end-to-end test suite (#41)
    • runnable on CI (GitHub Action) for Sqlite (#25)

Aggregate APIs

Add a new trait AggregatorTrait to be impl by Selector*
count()
and sum avg max min by specifying a column to aggregate on

sea-orm-cli

What should be command line interface?

First, we should read config from .env.

  1. migrate command for schema migration
# to run all migrations
sea-orm-cli migrate
# to run one migration
sea-orm-cli migrate up
# to revert a migration
sea-orm-cli migrate down
  1. generate command for code generation
# to generate entity files
sea-orm-cli generate entity

Mysql f64 problem

I switched over to the mysql adaptor for one of the tests and I started getting the following error:

thread 'main' panicked at 'range end index 8 out of range for slice of length 4', /Users/sam/.cargo/registry/src/github.com-1ecc6299db9ec823/byteorder-1.4.3/src/lib.rs:2199:28

I tracked it down to the profit_margin in the bakery model which was using f64. Changing it to f32 fixed the panic.

Here is an extract of tests/common/bakery_chain/bakery.rs on the origin/ss/mysql_try branch:

impl EntityName for Entity {
    fn table_name(&self) -> &str {
        "bakery"
    }
}

#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
    pub id: i32,
    pub name: String,
    pub profit_margin: f64,
}

More sophisticated CI tests

Running CI tests for all the combinations of below...

  • databases
    • SQLite
    • MySQL (version: 8.0, 5.7)
    • MariaDB (version: 10.5, 10.4)
    • Postgres (version: 12.3, 10.13)
  • runtimes
    • actix-native-tls
    • async-std-native-tls
    • tokio-native-tls
    • actix-rustls
    • async-std-rustls
    • tokio-rustls

Escape column names that conflict with Rust keywords in CLI

When generating models with the sea-orm-cli, if you have a column with a name that conflicts with a Rust keyword, the cli crashes and is not handled correctly.

For example, an SQL table:

| column_name | data_type |
|-------------------------|
| id          | serial    |
| type        | varchar   |

I get an error:

11 | ... { pub id : i64 , pub type : Option < String > }
   |                          ^^^^ expected identifier, found keyword

This is caused because the column type conflicts with the Rust keyword type.

Any rust columns conflicting should be renamed to resolve this issue.
https://doc.rust-lang.org/reference/keywords.html

Support schemas in queries

As far as I'm aware, there seems to be a lack of support for querying from schemas. For example, switching from public schema to a different schema.

A possible solution is that EntityName should support a new function called schema_name perhaps:

pub trait EntityName {
    fn schema_name(&self) -> Option<&str> {
        None
    }
}

Where None would not include a schema selector in the queries.. and Some(&str) would append the schema before the table name in queries.

This would change queries from:

SELECT "users"."id", "users"."email" FROM "users" ...

to:

SELECT "user"."users"."id", "user"."users"."email" FROM "user"."users" ...
-- or
SELECT "public"."users"."id", "public"."users"."email" FROM "public"."users" ...

Support non snake-case column name

We assume all columns in database table are in snack-case.

#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
    Id,
    Name,
}

We need to support an optional derive attribute to override this assumption.

#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
#[DeriveColumn(case = "camel")]
pub enum Column {
    Id,
    Name,
}

Or, customize column name of each variants

#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
    #[DeriveColumn(name = "ID")]
    Id,
    Name,
}

SelectToMany

While the SelectTwo we have now works well for one-to-one relation i.e. Cake <> Fruit,

find cakes and fruits: SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`, `fruit`.`id` AS `B_id`, `fruit`.`name` AS `B_name`, `fruit`.`cake_id` AS `B_cake_id` FROM `cake` LEFT JOIN `fruit` ON `cake`.`id` = `fruit`.`cake_id`

(Model { id: 1, name: "New York Cheese" }, Model { id: 2, name: "Rasberry", cake_id: Some(1) })

(Model { id: 1, name: "New York Cheese" }, Model { id: 1, name: "Blueberry", cake_id: Some(1) })

(Model { id: 2, name: "Chocolate Forest" }, Model { id: 3, name: "Strawberry", cake_id: Some(2) })

but for one-to-many relations, it would duplicate the left hand side.

So it'd be perfect if there is a selector that can consolidate the rows, returning:

(Model { id: 1, name: "New York Cheese" }, 
vec![ Model { id: 2, name: "Rasberry", cake_id: Some(1) }, Model { id: 1, name: "Blueberry", cake_id: Some(1) }])

(Model { id: 2, name: "Chocolate Forest" }, vec![ Model { id: 3, name: "Strawberry", cake_id: Some(2) }])

This is trivial to implement for SelectTwo, but to generalize to SelectThree, it will be more complex, requiring us to obtain the graph structure of the join path beforehand.

Error for sqlx_migrations table when generating entities

Hi guys!

Bumped in to a tiny error just now when trying to generate entities for my Postgres database. I've used sqlx-cli for the migrations.

โฏ sea-orm-cli generate entity
Error writing files: failed to resolve mod `sqlx_migrations`: /Users/nilskanevad/projects/kekou-api does not exist

The default table name for sqlx migrations is _sqlx_migrations - Notice the underscore.

The codegen creates a _sqlx_migrations.rs file but the table_name is getting set to sqlx_migrations

//! src/entity/_sqlx_migrations.rs
impl EntityName for Entity {
  fn table_name(&self) -> &str {
    "sqlx_migrations"
  }
}

After changing the table name in my DB to sqlx_migrations instead of _sqlx_migrations and generating the entities again it works without a issue.

So, it seems that somewhere, somehow the cli/codegen gets confused by the "_" in the tablename. Can't find where in the code though

Represent several relations between same types

Example problematic cases:

  1. Entities A et B have several different many-to-many relations
  2. Entity A has several one-to-many relations to entity B (Eg: COUPLE.member1 -> USER and COUPLE.member2 -> USER)

In these cases there are several issues:

  1. the codegen uses A/B entity names to generate Relation variants, so we obtain duplicate variants. I guess it could use the join table or foreign key entity name
  2. the Related trait is typed on A/B entities, so same issue. So I believe fixing this requires a breaking conception change...

TryGetable for Option<T> Should Handle Errors

We should not ignore any errors when decoding db result.

match row.try_get(column.as_str()) { 
    Ok(v) => Ok(Some(v)), 
    // Error ignored
    Err(_) => Ok(None), 
} 

For example:

impl TryGetable for Option<$type> {
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, DbErr> {
let column = format!("{}{}", pre, col);
match &res.row {
#[cfg(feature = "sqlx-mysql")]
QueryResultRow::SqlxMySql(row) => {
use sqlx::Row;
match row.try_get(column.as_str()) {
Ok(v) => Ok(Some(v)),
Err(_) => Ok(None),
}
}
#[cfg(feature = "sqlx-postgres")]
QueryResultRow::SqlxPostgres(row) => {
use sqlx::Row;
match row.try_get(column.as_str()) {
Ok(v) => Ok(Some(v)),
Err(_) => Ok(None),
}
}
#[cfg(feature = "sqlx-sqlite")]
QueryResultRow::SqlxSqlite(row) => {
use sqlx::Row;
match row.try_get(column.as_str()) {
Ok(v) => Ok(Some(v)),
Err(_) => Ok(None),
}
}
#[cfg(feature = "mock")]
QueryResultRow::Mock(row) => match row.try_get(column.as_str()) {
Ok(v) => Ok(Some(v)),
Err(_) => Ok(None),
},
}
}
}

Pagination API

A new struct Paginator that wraps a Select, that would:

  1. lazily COUNT the total number of records and number of pages
  2. perform paging by LIMIT and OFFSET

A helper method to convert a select into a paginator: (something like)

fn paginate(page_size: i32) -> Paginator<T>

The Paginator should impl the Stream trait.

Ref:
https://docs.rs/futures-core/0.3.15/futures_core/stream/trait.Stream.html
https://tokio.rs/tokio/tutorial/streams
https://docs.rs/async-std/1.9.0/async_std/stream/trait.Stream.html

Test Suite

  • create schema online
  • CRUD testing
  • relational query
  • other advanced queries (GROUP BY, etc)
  • sequential operations (read, insert / update, read again and update again)
  • Refactor the test suite such that it can generalizes over Postgres and MySQL (if possible, partially to SQLite)

Ideally the test suite should:

  • cover every SeaORM API
  • multiple cases for each API (has result, no result etc)
  • use more data types (UUID, Decimal, JSON, DateTime, etc)
  • operations that are more complex and 'real world application'
  • runs on SQLite, MySQL & Postgres

Using `find_also_*()` with `group_by(Col)` with correct column alias for `Col`

let baked_for_customers = Baker::find()
    .find_also_linked(baker::BakedForCustomer)
    .group_by(baker::Column::Id)
    .all(&ctx.db)
    .await?;

With SQL statement

SELECT `baker`.`id`              AS `A_id`,
       `baker`.`name`            AS `A_name`,
       `baker`.`contact_details` AS `A_contact_details`,
       `baker`.`bakery_id`       AS `A_bakery_id`,
       `customer`.`id`           AS `B_id`,
       `customer`.`name`         AS `B_name`,
       `customer`.`notes`        AS `B_notes`
FROM `baker`
         LEFT JOIN `cakes_bakers` ON `baker`.`id` = `cakes_bakers`.`baker_id`
         LEFT JOIN `cake` ON `cakes_bakers`.`cake_id` = `cake`.`id`
         LEFT JOIN `lineitem` ON `cake`.`id` = `lineitem`.`cake_id`
         LEFT JOIN `order` ON `lineitem`.`order_id` = `order`.`id`
         LEFT JOIN `customer` ON `order`.`customer_id` = `customer`.`id`
GROUP BY `baker`.`id`

Use the `log` crate instead of `"debug-print"` feature

Currently SeaORM uses a feature flag called "debug-print" which simple executes a println with the query.

It might be nice to use the "log" / "env-logger" crates instead.

For example, replace

debug_print!("{}", stmt);

with

info!(target: "sea-orm-query", "{}", stmt);

The benefit of this is that apps using SeaORM can change logging through env vars rather than recompiling with a new feature flag.
For example:

$ RUST_LOG=sea-orm-query=info ./main

SelectThree up to SelectNine

Right now we have a tuple based selector for select one and two. Need to think about how to generalise and support up to arity 9 perhaps?

src/query/combine.rs

Implement `insert` & `update` for `ActiveModel`

To avoid manually exec the insert ActiveModel. In below, we cannot simply save it because this is a junction table and the composite primary key should be set during insert.

let bobby_cakes_bakers = cakes_bakers::ActiveModel {
cake_id: Set(chocolate_cake_res.last_insert_id as i32),
baker_id: Set(baker_bobby_res.last_insert_id as i32),
..Default::default()
};
CakesBakers::insert(bobby_cakes_bakers)
.exec(&ctx.db)
.await?;

Perhaps impl it here

impl ActiveModel {
pub async fn save(self, db: &sea_orm::DatabaseConnection) -> Result<Self, sea_orm::DbErr> {
sea_orm::save_active_model::<Self, Entity>(self, db).await
}
pub async fn delete(self, db: &sea_orm::DatabaseConnection) -> Result<sea_orm::DeleteResult, sea_orm::DbErr> {
sea_orm::delete_active_model::<Self, Entity>(self, db).await
}
}

Transforming an Entity into TableCreateStatement

pub async fn create_baker_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
.table(baker::Entity)
.if_not_exists()
.col(
ColumnDef::new(baker::Column::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(baker::Column::Name).string())
.col(ColumnDef::new(baker::Column::ContactDetails).json())
.col(ColumnDef::new(baker::Column::BakeryId).integer())
.foreign_key(
ForeignKey::create()
.name("FK_baker_bakery")
.from(baker::Entity, baker::Column::BakeryId)
.to(bakery::Entity, bakery::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned();

To avoid duplication of information, we can actually generate a SeaQuery SchemaStatement entirely from the Entity definition.
It would be useful in 2 scenarios:

  1. writing test cases i.e. original model is MySQL but we want to test in SQLite. this is particularly helpful if the migration files are written in raw SQL. we effectively automatically translated from MySQL to SQLite!

  2. writing SQLite applications. in this case, apps don't need a separate migration tool. everything should be done by the app itself. so it's actually helpful to automatically bootstrap a SQLite database from an Entity file

Issue with `count` only with postgres driver

Branch: ss/test_suite_refactor
Commit: 3a0e7f3

To run the test:

DATABASE_URL="postgres://root:root@localhost" cargo test --features sqlx-postgres --test bakery_chain_tests

Issue is that the test passes for mysql but fails for postgres, feature flagging allows this test to pass but then one of the unit tests fails and I'm not sure how to fix it.

Tried changing executer/paginator.rs:66 like this:

        let num_items = if cfg!(feature = "sqlx-postgres") {
            result.try_get::<i64>("", "num_items")? as usize
        } else {
            result.try_get::<i32>("", "num_items")? as usize
        };

but then the unit tests fail.
This is blocking further work with the test suite.

Compile error '`Sync` is not implemented' under tokio

Related issue: SeaQL/sea-query#105

With the following function in a tokio async runtime, I get an error saying:

future created by async block is not `Send`
help: the trait `Sync` is not implemented for `(dyn Iden + 'static)`
async fn update(
    db: &DatabaseConnection,
    active_model: model::ActiveModel,
) -> Result<bool, Box<dyn std::error::Error>> {
    Entity::update(active_model)
        .exec(db)
        .await
        .map_err(Box::new)?;

    Ok(true)
}

SeaORM does not seem to be thread safe. Perhaps this is due to sea-query's Iden type.

How to implement From<Option<T>> for Value where T is my custom value?

Let's say I have a field in my table, something like

status: CHAR(1) NOT NULL

status can have those values: A (active), D (deleted), Q (queued), E (executed).
I want to map the field to a Rust enum, I can easily do:

pub enum Status {
  Active,
  Deleted,
  Queued,
  Executed,
}

impl From<Status> for Value {
  fn from(s: Status) -> Value {
    Value::from(match s {
      Status::Active => "A",
      Status::Deleted => "D",
      Status::Queued => "Q",
      Status::Executed => "E",
    })
  }
}

and everything works fine.

But if my status is, instead

status: CHAR(1) DEFAULT NULL

In my Model I would have to use Option< Status>, and I cant impl From< Option< Status>> for Value, because neither Option or Value comes from my crate.

Maybe you could solve removing every impl From< Option< Something>> for Value and replacing it with a simple impl< T: Into< Value>> From< Option< T>> for Value, I don't know if there would be conflicts, but this way you unlock a lot of custom types potentials

`InsertResult` to return the primary key's type

Currently, the last_insert_id from InsertResult simply uses the ExecResult's last_insert_id.
However, at least for Postgres with returning, we should be able to retrieve the primary key column for the Entity and return the value in its native type. For example, a uuid instead of an i64.

Support for row locking?

Hello, it's me again! xD

I just noticed there is no support for row locking (or at least I haven't find out).
Is it something you have excluded for a reason or simply you still haven't covered this feature?

Improve error messages

Now the error types are opaque.

Create a DbErr type to generalize QueryErr and ExecErr.

So that one ? can catch them all.

async fn find_all(db: &DbConn) -> Result<(), DbErr>

`InsertModel` and `UpdateModel`

Following up on #97 #94 #96

I have a rough idea:

  1. to annotate the attributes of Entity's Model, where each attribute has three possible 'requirements': auto_identity (auto generated primary key), required (implied and thus can be omitted) and optional

  2. from there, we can derive two more structs, InsertModel and UpdateModel
    For InsertModel, auto_identity will be omitted, and required fields will be wrapped with RequiredValue, which must be Set. Optional fields will be wrapped with ActiveValue, which can be Set or Unset.
    For UpdateModel, auto_identity will be wrapped with RequiredValue, while all other fields will be wrapped with ActiveValue (thus are optional)

  3. change the Insert and Update API such that they accept IntoInsertModel and IntoUpdateModel respectively. ActiveModel can still be converted automatically into InsertModel and UpdateModel, but will panic if the 'requirements' mismatch

Postgres support

Current development is MySQL only, and we need to catch up support for Postgres and ensure it's correctness.

  1. connector support
  2. account for behavioral difference from MySQL
  3. test suite support
  4. feature guarded driver support

Compact Entity format

I was trying your crate, seems promising, but the way you choose to represent models is too verbose.
For example I've a DB table with 53 fields, using your system I've written like 210 LOC, repeating the same informations like 3 times.

I think everything could be done with a single proc_macro, that takes a standard Rust struct and replaces it with a module with the same name of the struct, with Model struct with more or less the same contents of the original struct, Column enum with structs fields as variants, automatic impl ColumnTrait for Column with a type mapping, where the mapping fails you can use a field notation to override it (comes handy for custom types), table name and primary key can be specified with a struct notation, what's missing?
Relations, well, this have to be specified externally, but you can point the enum with another notation, and if the notation isn't present you generate a void enum.

After that, if I can add something more, I often use Cow on my entities, because this way I'm not forced to clone Strings when updating/creating. With your system I can't, because Model doesn't support a lifetime. It's not a big problem, but it blocks also other custom types with a lifetime

Provide API for executing raw query

Seems that there is no proper way to query with raw SQL.

Maybe we can add some helper method to EntityTrait

pub trait EntityTrait {
    fn find_raw( ... ) -> ... {
        todo!()
    }

    fn insert_raw( ... ) -> ... {
        todo!()
    }

    fn update_raw( ... ) -> ... {
        todo!()
    }

    fn delete_raw( ... ) -> ... {
        todo!()
    }
}

To support usage like

let stmt = Statement::from_string(
    DatabaseBackend::Postgres,
    r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT 1"#
);
let _ = fruit::Entity::find_raw(stmt).one(db).await?;

Correctly insert `ActiveModel::default()`

let cake = cake::ActiveModel { ..Default::default() };
Cake::insert(cake).exec(db).await?;

This returns a database error:

Query Error: error returned from database: syntax error at or near ")"

Because the generated query is:

INSERT INTO "cakes" () VALUES () RETURNING "id" AS "last_insert_id"

@acidic9 thanks for reporting this!

Support relation that join on multiple columns

.from and .to in RelationBuilder should take IntoIdentity instead of E::Column

impl RelationTrait for Relation {
    fn def(&self) -> RelationDef {
        match self {
            Self::Fruit => Entity::has_many(super::fruit::Entity)
                .from(Column::Id)
                .to(super::fruit::Column::CakeId)
                .into(),
        }
    }
}
impl<E, R> RelationBuilder<E, R>
where
    E: EntityTrait,
    R: EntityTrait,
{
    pub(crate) fn new(rel_type: RelationType, from: E, to: R) -> Self {
        Self {
            entities: PhantomData,
            rel_type,
            from_tbl: from.into_iden(),
            to_tbl: to.into_iden(),
            from_col: None,
            to_col: None,
        }
    }

    pub fn from(mut self, identifier: E::Column) -> Self {
        self.from_col = Some(identifier.into_identity());
        self
    }

    pub fn to(mut self, identifier: R::Column) -> Self {
        self.to_col = Some(identifier.into_identity());
        self
    }
}

Support postgres Uuid

I have a column of the following type:

"id" uuid DEFAULT uuid_generate_v4()

And I cannot find a row by it's ID.
I tried both of the following:

find_by_id("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
// SELECT "users"."name" FROM "users" WHERE "users"."id" = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' LIMIT 1
// Error: Query Error: error returned from database: operator does not exist: uuid = text

find_by_id(uuid::Uuid::from_str("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11").unwrap())
// ^^^^^^ the trait `From<uuid::Uuid>` is not implemented for `sea_orm::Value`

Is there any way to currently filter a row by UUID?

Bakery Schema Design

To design a schema which will be used in our test suite, tutorial and demo web app (REST API).
Covering all datatypes (numeric, strings, currency, datetime etc) and relations (1-1, 1-N, M-N).
Some 'static data' that don't change so often (like products) and operations (like sales).
In total around 10~20 entities (excluding junction table) ?

Entity Generator

Generating entity file for each db table.

  • Entity
  • Model
    • mapping db column type to Rust type
    • find_* helper function
  • Column
    • ColumnTrait column def
  • PrimaryKey
  • Relation
    • RelationTrait relation def
  • Related

Work plan in progress...

use crate as sea_orm;
use crate::entity::prelude::*;

#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
#[table = "cake"]
pub struct Entity;

#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
    pub id: i32,
    pub name: String,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
    Id,
    Name,
}

#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
    Id,
}

#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
    Fruit,
}

impl ColumnTrait for Column {
    type EntityName = Entity;

    fn def(&self) -> ColumnType {
        match self {
            Self::Id => ColumnType::Integer(None),
            Self::Name => ColumnType::String(None),
        }
    }
}

impl RelationTrait for Relation {
    fn def(&self) -> RelationDef {
        match self {
            Self::Fruit => Entity::has_many(super::fruit::Entity)
                .from(Column::Id)
                .to(super::fruit::Column::CakeId)
                .into(),
        }
    }
}

impl Related<super::fruit::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::Fruit.def()
    }
}

impl Related<super::filling::Entity> for Entity {
    fn to() -> RelationDef {
        super::cake_filling::Relation::Filling.def()
    }

    fn via() -> Option<RelationDef> {
        Some(super::cake_filling::Relation::Cake.def().rev())
    }
}

impl Model {
    pub fn find_fruit(&self) -> Select<super::fruit::Entity> {
        Entity::find_related().belongs_to::<Entity>(self)
    }

    pub fn find_filling(&self) -> Select<super::filling::Entity> {
        Entity::find_related().belongs_to::<Entity>(self)
    }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.