Merge branch 'entity-format' into entity_model_macro

This commit is contained in:
Marco Napetti 2021-09-07 10:09:34 +02:00 committed by GitHub
commit a4d70dfa55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 3021 additions and 364 deletions

View File

@ -11,6 +11,24 @@ env:
jobs:
clippy:
name: Clippy
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-targets --all
compile-sqlite:
name: Compile SQLite
runs-on: ubuntu-20.04
@ -152,7 +170,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
path: [async-std, tokio]
path: [async-std, tokio, rocket_example]
steps:
- uses: actions/checkout@v2

39
ARCHITECTURE.md Normal file
View File

@ -0,0 +1,39 @@
# Architecture
To understand the architecture of SeaORM, let's discuss what is an ORM. ORM exists to provide abstractions over common operations you would do against a database and hide the implementation details like the actual SQL queries.
With a good ORM, you shouldn't bother to look under the API surface. Until you do. I hear you say *'abstraction leaks'*, and yes, it does.
The approach SeaORM takes is **'layered abstraction'**, where you'd dig one layer beneath if you want to. That's why we made SeaQuery into a standalone repository. It's useful on its own, and with a public API surface and a separate namespace, it's far more difficult to create confusing internal APIs than a monolithic approach.
The central idea in SeaORM is nearly everything is runtime configurable. At compile time, it does not know what the underlying database is.
What benefits does database-agnostic bring? For example, you can:
1. Make your app work on any database, depending on runtime configuration
1. Use the same models and transfer them across different databases
1. Share entities across different projects by creating a 'data structure crate', where the database is chosen by downstream 'behaviour crates'
The API of SeaORM is not a thin shell, but consist of layers, with each layer underneath being less abstract.
There are different stages when the API is being utilized.
So there are two dimensions to navigate the SeaORM code base, **'stage'** and **'abstractness'**.
First is the declaration stage. Entities and relations among them are being defined with the `EntityTrait`, `ColumnTrait` & `RelationTrait` etc.
Second is the query building stage.
The top most layer is `Entity`'s `find*`, `insert`, `update` & `delete` methods, where you can intuitively perform basic CRUD operations.
One layer down, is the `Select`, `Insert`, `Update` & `Delete` structs, where they each have their own API for more advanced operations.
One layer down, is the SeaQuery `SelectStatement`, `InsertStatement`, `UpdateStatement` & `DeleteStatement`, where they have a rich API for you to fiddle with the SQL syntax tree.
Third is the execution stage. A separate set of structs, `Selector`, `Inserter`, `Updater` & `Deleter`, are responsible for executing the statements against a database connection.
Finally is the resolution stage, when query results are converted into Rust structs for consumption.
Because only the execution and resolution stages are database specific, we have the possibility to use a different driver by replacing those.
I imagine some day, we will support a number of databases, with a matrix of different syntaxes, protocols and form-factors.

View File

@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## 0.2.1 - 2021-09-04
- Update dependencies
## 0.2.0 - 2021-09-03
- [[#37]] Rocket example
- [[#114]] `log` crate and `env-logger`
- [[#103]] `InsertResult` to return the primary key's type
- [[#89]] Represent several relations between same types by `Linked`
- [[#59]] Transforming an Entity into `TableCreateStatement`
[#37]: https://github.com/SeaQL/sea-orm/issues/37
[#114]: https://github.com/SeaQL/sea-orm/issues/114
[#103]: https://github.com/SeaQL/sea-orm/issues/103
[#89]: https://github.com/SeaQL/sea-orm/issues/89
[#59]: https://github.com/SeaQL/sea-orm/issues/59
## 0.1.3 - 2021-08-30
- [[#108]] Remove impl TryGetable for Option<T>

View File

@ -1,13 +1,9 @@
[workspace]
members = [
".",
"sea-orm-macros",
"sea-orm-codegen",
]
members = [".", "sea-orm-macros", "sea-orm-codegen"]
[package]
name = "sea-orm"
version = "0.1.3"
version = "0.2.1"
authors = ["Chris Tsang <tyt2y7@gmail.com>"]
edition = "2018"
description = "🐚 An async & dynamic ORM for Rust"
@ -30,15 +26,14 @@ async-stream = { version = "^0.3" }
chrono = { version = "^0", optional = true }
futures = { version = "^0.3" }
futures-util = { version = "^0.3" }
log = { version = "^0.4", optional = true }
rust_decimal = { version = "^1", optional = true }
sea-orm-macros = { version = "^0.1.1", optional = true }
sea-query = { version = "^0.15", features = ["thread-safe"] }
sea-orm-macros = { version = "^0.2", path = "sea-orm-macros", optional = true }
sea-query = { version = "^0.16", features = ["thread-safe"] }
sea-strum = { version = "^0.21", features = ["derive", "sea-orm"] }
serde = { version = "^1.0", features = ["derive"] }
sqlx = { version = "^0.5", optional = true }
sqlx-core = { version = "^0.5", optional = true }
sqlx-macros = { version = "^0.5", optional = true }
serde_json = { version = "^1", optional = true }
sqlx = { version = "^0.5", optional = true }
uuid = { version = "0.8", features = ["serde", "v4"], optional = true }
[dev-dependencies]
@ -49,10 +44,12 @@ tokio = { version = "^1.6", features = ["full"] }
actix-rt = { version = "2.2.0" }
maplit = { version = "^1" }
rust_decimal_macros = { version = "^1" }
env_logger = { version = "^0.9" }
sea-orm = { path = ".", features = ["debug-print"] }
pretty_assertions = { version = "^0.7" }
[features]
debug-print = []
debug-print = ["log"]
default = [
"macros",
"mock",

View File

@ -1,4 +1,4 @@
# Design Goals
# Design
We are heavily inspired by ActiveRecord, Eloquent and TypeORM.
@ -20,7 +20,7 @@ After some bitterness we realized it is not possible to capture everything compi
want to encounter problems at run time either. The solution is to perform checking at 'test time' to
uncover problems. These checks will be removed at production so there will be no run time penalty.
## Readability
## API style
### Turbofish and inference
@ -66,3 +66,34 @@ we'd prefer having a builder and stating the params explicitly:
```rust
has_many(cake::Entity).from(cake::Column::Id).to(fruit::Column::CakeId)
```
### Method overloading
Consider the following two methods, which accept the same parameter but in different forms:
```rust
fn method_with_model(m: Model) { ... }
fn method_with_active_model(a: ActiveModel) { ... }
```
We would define a trait
```rust
pub trait IntoActiveModel {
fn into_active_model(self) -> ActiveModel;
}
```
Such that `Model` and `ActiveModel` both impl this trait.
In this way, we can overload the two methods:
```rust
pub fn method<A>(a: A)
where
A: IntoActiveModel,
{
let a: ActiveModel = a.into_active_model();
...
}
```

View File

@ -20,13 +20,9 @@
SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust.
```markdown
This is an early release of SeaORM, the API is not stable yet.
```
[![Getting Started](https://img.shields.io/badge/Getting%20Started-blue)](https://www.sea-ql.org/SeaORM/docs/index)
[![Examples](https://img.shields.io/badge/Examples-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/sqlx)
[![Starter Kit](https://img.shields.io/badge/Starter%20Kit-green)](https://github.com/SeaQL/sea-orm/issues/37)
[![Examples](https://img.shields.io/badge/Examples-green)](https://github.com/SeaQL/sea-orm/tree/master/examples)
[![Rocket Example](https://img.shields.io/badge/Rocket%20Example-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example)
[![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv)
## Features
@ -68,10 +64,8 @@ let cheese: cake::Model = cheese.unwrap();
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?;
let cake_with_fruits: Vec<(cake::Model, Vec<fruit::Model>)> =
Cake::find().find_with_related(Fruit).all(db).await?;
```
### Insert
@ -87,7 +81,7 @@ let pear = fruit::ActiveModel {
};
// insert one
let res: InsertResult = Fruit::insert(pear).exec(db).await?;
let res = Fruit::insert(pear).exec(db).await?;
println!("InsertResult: {}", res.last_insert_id);
@ -108,7 +102,7 @@ let pear: fruit::ActiveModel = Fruit::update(pear).exec(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::Null))
.col_expr(fruit::Column::CakeId, Expr::value(Value::Int(None)))
.filter(fruit::Column::Name.contains("Apple"))
.exec(db)
.await?;
@ -148,6 +142,13 @@ fruit::Entity::delete_many()
.await?;
```
## Learn More
1. [Design](https://github.com/SeaQL/sea-orm/tree/master/DESIGN.md)
1. [Architecture](https://github.com/SeaQL/sea-orm/tree/master/ARCHITECTURE.md)
1. [Compare with Diesel](https://www.sea-ql.org/SeaORM/docs/internal-design/diesel)
## License
Licensed under either of

View File

@ -28,6 +28,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = (i32, i32);
fn auto_increment() -> bool {
false
}

View File

@ -27,6 +27,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}

View File

@ -29,6 +29,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}

View File

@ -20,7 +20,7 @@ pub async fn insert_and_update(db: &DbConn) -> Result<(), DbErr> {
name: Set("pear".to_owned()),
..Default::default()
};
let res: InsertResult = Fruit::insert(pear).exec(db).await?;
let res = Fruit::insert(pear).exec(db).await?;
println!();
println!("Inserted: last_insert_id = {}\n", res.last_insert_id);

View File

@ -0,0 +1,34 @@
[package]
name = "sea-orm-rocket-example"
version = "0.1.0"
authors = ["Sam Samai <sam@studio2pi.com.au>"]
edition = "2018"
publish = false
[workspace]
[dependencies]
async-stream = { version = "^0.3" }
async-trait = { version = "0.1" }
futures = { version = "^0.3" }
futures-util = { version = "^0.3" }
rocket = { git = "https://github.com/SergioBenitez/Rocket.git", features = [
"json",
] }
rocket_db_pools = { git = "https://github.com/SergioBenitez/Rocket.git" }
rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket.git", features = [
"tera",
] }
# remove `path = ""` in your own project
sea-orm = { path = "../../", version = "^0.2" }
serde_json = { version = "^1" }
[dependencies.sqlx]
version = "^0.5"
default-features = false
features = ["macros", "offline", "migrate"]
[features]
default = ["sqlx-postgres"]
sqlx-mysql = ["sea-orm/sqlx-mysql", "rocket_db_pools/sqlx_mysql"]
sqlx-postgres = ["sea-orm/sqlx-postgres", "rocket_db_pools/sqlx_postgres"]

View File

@ -0,0 +1,11 @@
![screenshot](Screenshot.png)
# Rocket with SeaORM example app
1. Modify the `url` var in `Rocket.toml` to point to your chosen database
1. Turn on the appropriate database feature for your chosen db in `Cargo.toml` (the `default = ["sqlx-postgres"]` line)
1. `cargo run` to start the server
1. Open in browser after seeing the `🚀 Rocket has launched from http://localhost:8000` line

View File

@ -0,0 +1,11 @@
[default]
template_dir = "templates/"
[default.databases.rocket_example]
# Mysql
# make sure to enable "sqlx-mysql" feature in Cargo.toml, i.e default = ["sqlx-mysql"]
# url = "mysql://root:@localhost/rocket_example"
# Postgres
# make sure to enable "sqlx-postgres" feature in Cargo.toml, i.e default = ["sqlx-postgres"]
url = "postgres://root:root@localhost/rocket_example"

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,157 @@
#[macro_use]
extern crate rocket;
use rocket::fairing::{self, AdHoc};
use rocket::form::{Context, Form};
use rocket::fs::{relative, FileServer};
use rocket::request::FlashMessage;
use rocket::response::{Flash, Redirect};
use rocket::{Build, Request, Rocket};
use rocket_db_pools::{sqlx, Connection, Database};
use rocket_dyn_templates::{context, Template};
use sea_orm::entity::*;
use sea_orm::QueryOrder;
mod pool;
use pool::RocketDbPool;
mod setup;
#[derive(Database, Debug)]
#[database("rocket_example")]
struct Db(RocketDbPool);
type Result<T, E = rocket::response::Debug<sqlx::Error>> = std::result::Result<T, E>;
mod post;
pub use post::Entity as Post;
#[get("/new")]
fn new() -> Template {
Template::render("new", &Context::default())
}
#[post("/", data = "<post_form>")]
async fn create(conn: Connection<Db>, post_form: Form<post::Model>) -> Flash<Redirect> {
let post = post_form.into_inner();
let _post = post::ActiveModel {
title: Set(post.title.to_owned()),
text: Set(post.text.to_owned()),
..Default::default()
}
.save(&conn)
.await
.expect("could not insert post");
Flash::success(Redirect::to("/"), "Post successfully added.")
}
#[post("/<id>", data = "<post_form>")]
async fn update(conn: Connection<Db>, id: i64, post_form: Form<post::Model>) -> Flash<Redirect> {
let post: post::ActiveModel = Post::find_by_id(id)
.one(&conn)
.await
.unwrap()
.unwrap()
.into();
let post_data = post_form.into_inner();
let _edited_post = post::ActiveModel {
id: post.id,
title: Set(post_data.title.to_owned()),
text: Set(post_data.text.to_owned()),
}
.save(&conn)
.await
.expect("could not edit post");
Flash::success(Redirect::to("/"), "Post successfully edited.")
}
#[get("/")]
async fn list(conn: Connection<Db>, flash: Option<FlashMessage<'_>>) -> Template {
let posts = Post::find()
.order_by_asc(post::Column::Id)
.all(&conn)
.await
.expect("could not retrieve posts")
.into_iter()
.collect::<Vec<_>>();
let flash = flash.map(FlashMessage::into_inner);
Template::render(
"index",
context! {
posts: posts,
flash: flash,
},
)
}
#[get("/<id>")]
async fn edit(conn: Connection<Db>, id: i64) -> Template {
let post: Option<post::Model> = Post::find_by_id(id)
.one(&conn)
.await
.expect("could not find post");
Template::render(
"edit",
context! {
post: post,
},
)
}
#[delete("/<id>")]
async fn delete(conn: Connection<Db>, id: i32) -> Flash<Redirect> {
let post: post::ActiveModel = Post::find_by_id(id)
.one(&conn)
.await
.unwrap()
.unwrap()
.into();
let _result = post.delete(&conn).await.unwrap();
Flash::success(Redirect::to("/"), "Post successfully deleted.")
}
#[delete("/")]
async fn destroy(conn: Connection<Db>) -> Result<()> {
let _result = Post::delete_many().exec(&conn).await.unwrap();
Ok(())
}
#[catch(404)]
pub fn not_found(req: &Request<'_>) -> Template {
Template::render(
"error/404",
context! {
uri: req.uri()
},
)
}
async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
let db_url = Db::fetch(&rocket).unwrap().db_url.clone();
let conn = sea_orm::Database::connect(&db_url).await.unwrap();
let _create_post_table = setup::create_post_table(&conn).await;
Ok(rocket)
}
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(Db::init())
.attach(AdHoc::try_on_ignite("Migrations", run_migrations))
.mount("/", FileServer::from(relative!("/static")))
.mount(
"/",
routes![new, create, delete, destroy, list, edit, update],
)
.register("/", catchers![not_found])
.attach(Template::fairing())
}

View File

@ -0,0 +1,27 @@
use async_trait::async_trait;
use rocket_db_pools::{rocket::figment::Figment, Config};
#[derive(Debug)]
pub struct RocketDbPool {
pub db_url: String,
}
#[async_trait]
impl rocket_db_pools::Pool for RocketDbPool {
type Error = sea_orm::DbErr;
type Connection = sea_orm::DatabaseConnection;
async fn init(figment: &Figment) -> Result<Self, Self::Error> {
let config = figment.extract::<Config>().unwrap();
let db_url = config.url;
Ok(RocketDbPool {
db_url: db_url.to_owned(),
})
}
async fn get(&self) -> Result<Self::Connection, Self::Error> {
Ok(sea_orm::Database::connect(&self.db_url).await.unwrap())
}
}

View File

@ -0,0 +1,65 @@
use rocket::serde::{Deserialize, Serialize};
use sea_orm::entity::prelude::*;
#[derive(Copy, Clone, Default, Debug, DeriveEntity, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct Entity;
impl EntityName for Entity {
fn table_name(&self) -> &str {
"posts"
}
}
#[derive(
Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Deserialize, Serialize, FromForm,
)]
#[serde(crate = "rocket::serde")]
pub struct Model {
#[serde(skip_deserializing, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub title: String,
pub text: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Title,
Text,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Title => ColumnType::String(None).def(),
Self::Text => ColumnType::String(None).def(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,33 @@
use sea_orm::sea_query::{ColumnDef, TableCreateStatement};
use sea_orm::{error::*, sea_query, DbConn, ExecResult};
async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result<ExecResult, DbErr> {
let builder = db.get_database_backend();
db.execute(builder.build(stmt)).await
}
pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
.table(super::post::Entity)
.if_not_exists()
.col(
ColumnDef::new(super::post::Column::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(
ColumnDef::new(super::post::Column::Title)
.string()
.not_null(),
)
.col(
ColumnDef::new(super::post::Column::Text)
.string()
.not_null(),
)
.to_owned();
create_table(db, &stmt).await
}

View File

@ -0,0 +1,427 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

View File

@ -0,0 +1,421 @@
/*
* Skeleton V2.0.4
* Copyright 2014, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* https://opensource.org/licenses/mit-license.php
* 12/29/2014
*/
/* Table of contents
- Grid
- Base Styles
- Typography
- Links
- Buttons
- Forms
- Lists
- Code
- Tables
- Spacing
- Utilities
- Clearing
- Media Queries
*/
/* Grid
*/
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box; }
.column,
.columns {
width: 100%;
float: left;
box-sizing: border-box; }
/* For devices larger than 400px */
@media (min-width: 400px) {
.container {
width: 85%;
padding: 0; }
}
/* For devices larger than 550px */
@media (min-width: 550px) {
.container {
width: 80%; }
.column,
.columns {
margin-left: 4%; }
.column:first-child,
.columns:first-child {
margin-left: 0; }
.one.column,
.one.columns { width: 4.66666666667%; }
.two.columns { width: 13.3333333333%; }
.three.columns { width: 22%; }
.four.columns { width: 30.6666666667%; }
.five.columns { width: 39.3333333333%; }
.six.columns { width: 48%; }
.seven.columns { width: 56.6666666667%; }
.eight.columns { width: 65.3333333333%; }
.nine.columns { width: 74.0%; }
.ten.columns { width: 82.6666666667%; }
.eleven.columns { width: 91.3333333333%; }
.twelve.columns { width: 100%; margin-left: 0; }
.one-third.column { width: 30.6666666667%; }
.two-thirds.column { width: 65.3333333333%; }
.one-half.column { width: 48%; }
/* Offsets */
.offset-by-one.column,
.offset-by-one.columns { margin-left: 8.66666666667%; }
.offset-by-two.column,
.offset-by-two.columns { margin-left: 17.3333333333%; }
.offset-by-three.column,
.offset-by-three.columns { margin-left: 26%; }
.offset-by-four.column,
.offset-by-four.columns { margin-left: 34.6666666667%; }
.offset-by-five.column,
.offset-by-five.columns { margin-left: 43.3333333333%; }
.offset-by-six.column,
.offset-by-six.columns { margin-left: 52%; }
.offset-by-seven.column,
.offset-by-seven.columns { margin-left: 60.6666666667%; }
.offset-by-eight.column,
.offset-by-eight.columns { margin-left: 69.3333333333%; }
.offset-by-nine.column,
.offset-by-nine.columns { margin-left: 78.0%; }
.offset-by-ten.column,
.offset-by-ten.columns { margin-left: 86.6666666667%; }
.offset-by-eleven.column,
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
.offset-by-one-third.column,
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
.offset-by-two-thirds.column,
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
.offset-by-one-half.column,
.offset-by-one-half.columns { margin-left: 52%; }
}
/* Base Styles
*/
/* NOTE
html is set to 62.5% so that all the REM measurements throughout Skeleton
are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
font-size: 62.5%; }
body {
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
line-height: 1.6;
font-weight: 400;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #222; }
/* Typography
*/
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 2rem;
font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
/* Larger than phablet */
@media (min-width: 550px) {
h1 { font-size: 5.0rem; }
h2 { font-size: 4.2rem; }
h3 { font-size: 3.6rem; }
h4 { font-size: 3.0rem; }
h5 { font-size: 2.4rem; }
h6 { font-size: 1.5rem; }
}
p {
margin-top: 0; }
/* Links
*/
a {
color: #1EAEDB; }
a:hover {
color: #0FA0CE; }
/* Buttons
*/
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
display: inline-block;
height: 38px;
padding: 0 30px;
color: #555;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 38px;
letter-spacing: .1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
color: #333;
border-color: #888;
outline: 0; }
.button.button-primary,
button.button-primary,
button.primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
color: #FFF;
background-color: #33C3F0;
border-color: #33C3F0; }
.button.button-primary:hover,
button.button-primary:hover,
button.primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
button.primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
color: #FFF;
background-color: #1EAEDB;
border-color: #1EAEDB; }
/* Forms
*/
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea,
select {
height: 38px;
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
background-color: #fff;
border: 1px solid #D1D1D1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none; }
textarea {
min-height: 65px;
padding-top: 6px;
padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
border: 1px solid #33C3F0;
outline: 0; }
label,
legend {
display: block;
margin-bottom: .5rem;
font-weight: 600; }
fieldset {
padding: 0;
border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
display: inline; }
label > .label-body {
display: inline-block;
margin-left: .5rem;
font-weight: normal; }
/* Lists
*/
ul {
list-style: circle inside; }
ol {
list-style: decimal inside; }
ol, ul {
padding-left: 0;
margin-top: 0; }
ul ul,
ul ol,
ol ol,
ol ul {
margin: 1.5rem 0 1.5rem 3rem;
font-size: 90%; }
li {
margin-bottom: 1rem; }
/* Code
*/
code {
padding: .2rem .5rem;
margin: 0 .2rem;
font-size: 90%;
white-space: nowrap;
background: #F1F1F1;
border: 1px solid #E1E1E1;
border-radius: 4px; }
pre > code {
display: block;
padding: 1rem 1.5rem;
white-space: pre; }
/* Tables
*/
th,
td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #E1E1E1; }
th:first-child,
td:first-child {
padding-left: 0; }
th:last-child,
td:last-child {
padding-right: 0; }
/* Spacing
*/
button,
.button {
margin-bottom: 1rem; }
input,
textarea,
select,
fieldset {
margin-bottom: 1.5rem; }
pre,
blockquote,
dl,
figure,
table,
p,
ul,
ol,
form {
margin-bottom: 2.5rem; }
/* Utilities
*/
.u-full-width {
width: 100%;
box-sizing: border-box; }
.u-max-full-width {
max-width: 100%;
box-sizing: border-box; }
.u-pull-right {
float: right; }
.u-pull-left {
float: left; }
/* Misc
*/
hr {
margin-top: 3rem;
margin-bottom: 3.5rem;
border-width: 0;
border-top: 1px solid #E1E1E1; }
/* Clearing
*/
/* Self Clearing Goodness */
.container:after,
.row:after,
.u-cf {
content: "";
display: table;
clear: both; }
/* Media Queries
*/
/*
Note: The best way to structure the use of media queries is to create the queries
near the relevant code. For example, if you wanted to change the styles for buttons
on small devices, paste the mobile query code up in the buttons section and style it
there.
*/
/* Larger than mobile */
@media (min-width: 400px) {}
/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {}
/* Larger than tablet */
@media (min-width: 750px) {}
/* Larger than desktop */
@media (min-width: 1000px) {}
/* Larger than Desktop HD */
@media (min-width: 1200px) {}

View File

@ -0,0 +1,73 @@
.field-error {
border: 1px solid #ff0000 !important;
}
.field-error-flash {
color: #ff0000;
display: block;
margin: -10px 0 10px 0;
}
.field-success {
border: 1px solid #5ab953 !important;
}
.field-success-flash {
color: #5ab953;
display: block;
margin: -10px 0 10px 0;
}
span.completed {
text-decoration: line-through;
}
form.inline {
display: inline;
}
form.link,
button.link {
display: inline;
color: #1eaedb;
border: none;
outline: none;
background: none;
cursor: pointer;
padding: 0;
margin: 0 0 0 0;
height: inherit;
text-decoration: underline;
font-size: inherit;
text-transform: none;
font-weight: normal;
line-height: inherit;
letter-spacing: inherit;
}
form.link:hover,
button.link:hover {
color: #0fa0ce;
}
button.small {
height: 20px;
padding: 0 10px;
font-size: 10px;
line-height: 20px;
margin: 0 2.5px;
}
.post:hover {
background-color: #bce2ee;
}
.post td {
padding: 5px;
width: 150px;
}
#delete-button {
color: red;
border-color: red;
}

Binary file not shown.

View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Rocket Todo Example</title>
<meta name="description" content="A todo application written in Rocket." />
<meta name="author" content="Sergio Benitez" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="//fonts.googleapis.com/css?family=Raleway:400,300,600"
rel="stylesheet"
type="text/css"
/>
<link rel="stylesheet" href="/css/normalize.css" />
<link rel="stylesheet" href="/css/skeleton.css" />
<link rel="stylesheet" href="/css/style.css" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
</head>
<body>
<div class="container">
<p><!--Nothing to see here --></p>
{#
<div class="row">
<h4>Rocket Todo</h4>
<form action="/todo" method="post">
<div class="ten columns">
<input
type="text"
placeholder="enter a task description..."
name="description"
id="description"
value=""
autofocus
class="u-full-width {% if msg %}field-{{msg.0}}{% endif %}"
/>
{% if msg %}
<small class="field-{{msg.0}}-msg">
{{ msg.1 }}
</small>
{% endif %}
</div>
<div class="two columns">
<input type="submit" value="add task" />
</div>
</form>
</div>
<div class="row">
<div class="twelve columns">
<ul>
{% for task in tasks %} {% if task.completed %}
<li>
<span class="completed">{{ task.description }}</span>
<form class="inline" action="/todo/{{ task.id }}" method="post">
<input type="hidden" name="_method" value="put" />
<button class="small" type="submit">undo</button>
</form>
<form class="inline" action="/todo/{{ task.id }}" method="post">
<input type="hidden" name="_method" value="delete" />
<button class="primary small" type="submit">delete</button>
</form>
</li>
{% else %}
<li>
<form class="link" action="/todo/{{ task.id }}" method="post">
<input type="hidden" name="_method" value="put" />
<button class="link" type="submit">
{{ task.description }}
</button>
</form>
</li>
{% endif %} {% endfor %}
</ul>
</div>
</div>
#} {% block content %}{% endblock content %}
</div>
</body>
</html>

View File

@ -0,0 +1,50 @@
{% extends "base" %} {% block content %}
<div class="row">
<h4>Edit Post</h4>
<div class="twelve columns">
<div class="ten columns">
<form action="/{{ post.id }}" method="post">
<div class="twelve columns">
<input
type="text"
placeholder="title"
name="title"
id="title"
value="{{ post.title }}"
autofocus
class="u-full-width"
/>
<input
type="text"
placeholder="content"
name="text"
id="text"
value="{{ post.text }}"
autofocus
class="u-full-width"
/>
</div>
<div class="twelve columns">
<div class="two columns">
<a href="/">
<input type="button" value="cancel" />
</a>
</div>
<div class="eight columns"></div>
<div class="two columns">
<input type="submit" value="save post" />
</div>
</div>
</form>
</div>
<div class="two columns">
<form action="/{{ post.id }}" method="post">
<div class="two columns">
<input type="hidden" name="_method" value="delete" />
<input id="delete-button" type="submit" value="delete post" />
</div>
</form>
</div>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>404 - tera</title>
</head>
<body>
<h1>404: Hey! There's nothing here.</h1>
The page at {{ uri }} does not exist!
</body>
</html>

View File

@ -0,0 +1,33 @@
{% extends "base" %} {% block content %}
<h1>Posts</h1>
{% if flash %}
<small class="field-{{ flash.0 }}-flash">
{{ flash.1 }}
</small>
{% endif %}
<table>
<tbody>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Text</th>
</tr>
</thead>
{% for post in posts %}
<tr class="post" onclick="window.location='/{{ post.id }}';">
<td>{{ post.id }}</td>
<td>{{ post.title }}</td>
<td>{{ post.text }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="twelve columns">
<a href="/new">
<input type="button" value="add post" />
</a>
</div>
{% endblock content %}

View File

@ -0,0 +1,38 @@
{% extends "base" %} {% block content %}
<div class="row">
<h4>New Post</h4>
<form action="/" method="post">
<div class="twelve columns">
<input
type="text"
placeholder="enter title"
name="title"
id="title"
value=""
autofocus
class="u-full-width"
/>
<input
type="text"
placeholder="enter content"
name="text"
id="text"
value=""
autofocus
class="u-full-width"
/>
</div>
<div class="twelve columns">
<div class="two columns">
<a href="/">
<input type="button" value="cancel" />
</a>
</div>
<div class="eight columns"></div>
<div class="two columns">
<input type="submit" value="save post" />
</div>
</div>
</form>
</div>
{% endblock content %}

View File

@ -27,6 +27,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
format_code_in_doc_comments=true

View File

@ -3,7 +3,7 @@
[package]
name = "sea-orm-cli"
version = "0.1.3"
version = "0.2.0"
authors = [ "Billy Chan <ccw.billy.123@gmail.com>" ]
edition = "2018"
description = "Command line utility for SeaORM"
@ -22,7 +22,7 @@ clap = { version = "^2.33.3" }
dotenv = { version = "^0.15" }
async-std = { version = "^1.9", features = [ "attributes" ] }
sea-orm = { version = "^0.1.2", features = [ "sqlx-all" ] }
sea-orm-codegen = { version = "^0.1.3" }
sea-orm-codegen = { version = "^0.2.0" }
sea-schema = { version = "^0.2.7", default-features = false, features = [
"sqlx-mysql",
"sqlx-postgres",

View File

@ -1,6 +1,6 @@
[package]
name = "sea-orm-codegen"
version = "0.1.3"
version = "0.2.0"
authors = ["Billy Chan <ccw.billy.123@gmail.com>"]
edition = "2018"
description = "Code Generator for SeaORM"

View File

@ -117,6 +117,31 @@ impl Entity {
format_ident!("{}", auto_increment)
}
pub fn get_primary_key_rs_type(&self) -> TokenStream {
let types = self
.primary_keys
.iter()
.map(|primary_key| {
self.columns
.iter()
.find(|col| col.name.eq(&primary_key.name))
.unwrap()
.get_rs_type()
.to_string()
})
.collect::<Vec<_>>();
if !types.is_empty() {
let value_type = if types.len() > 1 {
vec!["(".to_owned(), types.join(", "), ")".to_owned()]
} else {
types
};
value_type.join("").parse().unwrap()
} else {
TokenStream::new()
}
}
pub fn get_conjunct_relations_via_snake_case(&self) -> Vec<Ident> {
self.conjunct_relations
.iter()
@ -151,7 +176,7 @@ mod tests {
columns: vec![
Column {
name: "id".to_owned(),
col_type: ColumnType::String(None),
col_type: ColumnType::Integer(None),
auto_increment: false,
not_null: false,
unique: false,
@ -373,6 +398,16 @@ mod tests {
);
}
#[test]
fn test_get_primary_key_rs_type() {
let entity = setup();
assert_eq!(
entity.get_primary_key_rs_type().to_string(),
entity.columns[0].get_rs_type().to_string()
);
}
#[test]
fn test_get_conjunct_relations_via_snake_case() {
let entity = setup();

View File

@ -119,33 +119,18 @@ impl From<&ColumnDef> for Column {
Some(ty) => ty.clone(),
None => panic!("ColumnType should not be empty"),
};
let auto_increments: Vec<bool> = col_def
let auto_increment = col_def
.get_column_spec()
.iter()
.filter_map(|spec| match spec {
ColumnSpec::AutoIncrement => Some(true),
_ => None,
})
.collect();
let auto_increment = !auto_increments.is_empty();
let not_nulls: Vec<bool> = col_def
.any(|spec| matches!(spec, ColumnSpec::AutoIncrement));
let not_null = col_def
.get_column_spec()
.iter()
.filter_map(|spec| match spec {
ColumnSpec::NotNull => Some(true),
_ => None,
})
.collect();
let not_null = !not_nulls.is_empty();
let uniques: Vec<bool> = col_def
.any(|spec| matches!(spec, ColumnSpec::NotNull));
let unique = col_def
.get_column_spec()
.iter()
.filter_map(|spec| match spec {
ColumnSpec::UniqueKey => Some(true),
_ => None,
})
.collect();
let unique = !uniques.is_empty();
.any(|spec| matches!(spec, ColumnSpec::UniqueKey));
Self {
name,
col_type,

View File

@ -34,11 +34,6 @@ impl EntityTransformer {
.iter()
.map(|col_def| col_def.into())
.collect();
let unique_columns: Vec<String> = columns
.iter()
.filter(|col| col.unique)
.map(|col| col.name.clone())
.collect();
let relations = table_create
.get_foreign_key_create_stmts()
.iter()
@ -85,8 +80,13 @@ impl EntityTransformer {
false => {
let ref_table = rel.ref_table;
let mut unique = true;
for col in rel.columns.iter() {
if !unique_columns.contains(col) {
for column in rel.columns.iter() {
if !entity
.columns
.iter()
.filter(|col| col.unique)
.any(|col| col.name.as_str() == column)
{
unique = false;
break;
}

View File

@ -173,8 +173,11 @@ impl EntityWriter {
pub fn gen_impl_primary_key(entity: &Entity) -> TokenStream {
let primary_key_auto_increment = entity.get_primary_key_auto_increment();
let value_type = entity.get_primary_key_rs_type();
quote! {
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = #value_type;
fn auto_increment() -> bool {
#primary_key_auto_increment
}
@ -305,7 +308,7 @@ mod tests {
use sea_query::ColumnType;
use std::io::{self, BufRead, BufReader};
const ENTITY_FILES: [&'static str; 5] = [
const ENTITY_FILES: [&str; 5] = [
include_str!("../../tests/entity/cake.rs"),
include_str!("../../tests/entity/cake_filling.rs"),
include_str!("../../tests/entity/filling.rs"),

View File

@ -29,6 +29,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}

View File

@ -30,6 +30,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = (i32, i32);
fn auto_increment() -> bool {
false
}

View File

@ -29,6 +29,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}

View File

@ -31,6 +31,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}

View File

@ -31,6 +31,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}

View File

@ -1,6 +1,6 @@
[package]
name = "sea-orm-macros"
version = "0.1.1"
version = "0.2.0"
authors = [ "Billy Chan <ccw.billy.123@gmail.com>" ]
edition = "2018"
description = "Derive macros for SeaORM"

View File

@ -99,6 +99,10 @@ pub fn test(_: TokenStream, input: TokenStream) -> TokenStream {
#[test]
#(#attrs)*
fn #name() #ret {
let _ = ::env_logger::builder()
.filter_level(::log::LevelFilter::Debug)
.is_test(true)
.try_init();
crate::block_on!(async { #body })
}
)

View File

@ -75,7 +75,7 @@ impl DatabaseConnection {
DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.execute(stmt).await,
#[cfg(feature = "mock")]
DatabaseConnection::MockDatabaseConnection(conn) => conn.execute(stmt).await,
DatabaseConnection::Disconnected => panic!("Disconnected"),
DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())),
}
}
@ -89,7 +89,7 @@ impl DatabaseConnection {
DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.query_one(stmt).await,
#[cfg(feature = "mock")]
DatabaseConnection::MockDatabaseConnection(conn) => conn.query_one(stmt).await,
DatabaseConnection::Disconnected => panic!("Disconnected"),
DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())),
}
}
@ -103,7 +103,7 @@ impl DatabaseConnection {
DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.query_all(stmt).await,
#[cfg(feature = "mock")]
DatabaseConnection::MockDatabaseConnection(conn) => conn.query_all(stmt).await,
DatabaseConnection::Disconnected => panic!("Disconnected"),
DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())),
}
}

View File

@ -2,19 +2,22 @@ use crate::{
debug_print, error::*, DatabaseConnection, DbBackend, ExecResult, MockDatabase, QueryResult,
Statement, Transaction,
};
use std::fmt::Debug;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Mutex,
};
#[derive(Debug)]
pub struct MockDatabaseConnector;
#[derive(Debug)]
pub struct MockDatabaseConnection {
counter: AtomicUsize,
mocker: Mutex<Box<dyn MockDatabaseTrait>>,
}
pub trait MockDatabaseTrait: Send {
pub trait MockDatabaseTrait: Send + Debug {
fn execute(&mut self, counter: usize, stmt: Statement) -> Result<ExecResult, DbErr>;
fn query(&mut self, counter: usize, stmt: Statement) -> Result<Vec<QueryResult>, DbErr>;

View File

@ -10,8 +10,10 @@ use crate::{debug_print, error::*, executor::*, DatabaseConnection, Statement};
use super::sqlx_common::*;
#[derive(Debug)]
pub struct SqlxMySqlConnector;
#[derive(Debug)]
pub struct SqlxMySqlPoolConnection {
pool: MySqlPool,
}

View File

@ -10,8 +10,10 @@ use crate::{debug_print, error::*, executor::*, DatabaseConnection, Statement};
use super::sqlx_common::*;
#[derive(Debug)]
pub struct SqlxPostgresConnector;
#[derive(Debug)]
pub struct SqlxPostgresPoolConnection {
pool: PgPool,
}
@ -102,24 +104,11 @@ impl From<PgRow> for QueryResult {
impl From<PgQueryResult> for ExecResult {
fn from(result: PgQueryResult) -> ExecResult {
ExecResult {
result: ExecResultHolder::SqlxPostgres {
last_insert_id: 0,
rows_affected: result.rows_affected(),
},
result: ExecResultHolder::SqlxPostgres(result),
}
}
}
pub(crate) fn query_result_into_exec_result(res: QueryResult) -> Result<ExecResult, DbErr> {
let last_insert_id: i32 = res.try_get("", "last_insert_id")?;
Ok(ExecResult {
result: ExecResultHolder::SqlxPostgres {
last_insert_id: last_insert_id as u64,
rows_affected: 0,
},
})
}
fn sqlx_query(stmt: &Statement) -> sqlx::query::Query<'_, Postgres, PgArguments> {
let mut query = sqlx::query(&stmt.sql);
if let Some(values) = &stmt.values {

View File

@ -10,8 +10,10 @@ use crate::{debug_print, error::*, executor::*, DatabaseConnection, Statement};
use super::sqlx_common::*;
#[derive(Debug)]
pub struct SqlxSqliteConnector;
#[derive(Debug)]
pub struct SqlxSqlitePoolConnection {
pool: SqlitePool,
}

View File

@ -221,16 +221,18 @@ where
let exec = E::insert(am).exec(db);
let res = exec.await?;
// TODO: if the entity does not have auto increment primary key, then last_insert_id is a wrong value
if <E::PrimaryKey as PrimaryKeyTrait>::auto_increment() && res.last_insert_id != 0 {
// FIXME: Assumed valid last_insert_id is not equals to Default::default()
if <E::PrimaryKey as PrimaryKeyTrait>::auto_increment()
&& res.last_insert_id != <E::PrimaryKey as PrimaryKeyTrait>::ValueType::default()
{
let find = E::find_by_id(res.last_insert_id).one(db);
let found = find.await;
let model: Option<E::Model> = found?;
match model {
Some(model) => Ok(model.into_active_model()),
None => Err(DbErr::Exec(format!(
"Failed to find inserted item: {} {}",
"Failed to find inserted item: {}",
E::default().to_string(),
res.last_insert_id
))),
}
} else {

View File

@ -55,21 +55,21 @@ pub trait EntityTrait: EntityName {
where
R: EntityTrait,
{
RelationBuilder::new(RelationType::HasOne, Self::default(), related)
RelationBuilder::new(RelationType::HasOne, Self::default(), related, false)
}
fn has_one<R>(_: R) -> RelationBuilder<Self, R>
where
R: EntityTrait + Related<Self>,
{
RelationBuilder::from_rel(RelationType::HasOne, R::to().rev())
RelationBuilder::from_rel(RelationType::HasOne, R::to().rev(), true)
}
fn has_many<R>(_: R) -> RelationBuilder<Self, R>
where
R: EntityTrait + Related<Self>,
{
RelationBuilder::from_rel(RelationType::HasMany, R::to().rev())
RelationBuilder::from_rel(RelationType::HasMany, R::to().rev(), true)
}
/// Construct select statement to find one / all models
@ -138,12 +138,17 @@ pub trait EntityTrait: EntityName {
/// db.into_transaction_log(),
/// vec![
/// Transaction::from_sql_and_values(
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, vec![1u64.into()]
/// DbBackend::Postgres,
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#,
/// vec![1u64.into()]
/// ),
/// Transaction::from_sql_and_values(
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![]
/// DbBackend::Postgres,
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake""#,
/// vec![]
/// ),
/// ]);
/// ]
/// );
/// ```
fn find() -> Select<Self> {
Select::new()
@ -186,8 +191,11 @@ pub trait EntityTrait: EntityName {
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1"#, vec![11i32.into()]
/// )]);
/// DbBackend::Postgres,
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1"#,
/// vec![11i32.into()]
/// )]
/// );
/// ```
/// Find by composite key
/// ```

View File

@ -1,6 +1,6 @@
use std::str::FromStr;
use crate::{EntityName, IdenStatic, Iterable};
use sea_query::{DynIden, Expr, SeaRc, SelectStatement, SimpleExpr, Value};
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct ColumnDef {
@ -248,7 +248,11 @@ impl ColumnDef {
self
}
pub fn null(mut self) -> Self {
pub fn null(self) -> Self {
self.nullable()
}
pub fn nullable(mut self) -> Self {
self.null = true;
self
}

View File

@ -1,5 +1,6 @@
use crate::{ColumnTrait, EntityTrait, IdenStatic};
use sea_query::{DynIden, IntoIden};
use sea_query::{Alias, DynIden, Iden, IntoIden, SeaRc};
use std::fmt;
#[derive(Debug, Clone)]
pub enum Identity {
@ -8,6 +9,25 @@ pub enum Identity {
Ternary(DynIden, DynIden, DynIden),
}
impl Iden for Identity {
fn unquoted(&self, s: &mut dyn fmt::Write) {
match self {
Identity::Unary(iden) => {
write!(s, "{}", iden.to_string()).unwrap();
}
Identity::Binary(iden1, iden2) => {
write!(s, "{}", iden1.to_string()).unwrap();
write!(s, "{}", iden2.to_string()).unwrap();
}
Identity::Ternary(iden1, iden2, iden3) => {
write!(s, "{}", iden1.to_string()).unwrap();
write!(s, "{}", iden2.to_string()).unwrap();
write!(s, "{}", iden3.to_string()).unwrap();
}
}
}
}
pub trait IntoIdentity {
fn into_identity(self) -> Identity;
}
@ -19,6 +39,18 @@ where
fn identity_of(self) -> Identity;
}
impl IntoIdentity for String {
fn into_identity(self) -> Identity {
self.as_str().into_identity()
}
}
impl IntoIdentity for &str {
fn into_identity(self) -> Identity {
Identity::Unary(SeaRc::new(Alias::new(self)))
}
}
impl<T> IntoIdentity for T
where
T: IdenStatic,

View File

@ -6,6 +6,7 @@ mod model;
pub mod prelude;
mod primary_key;
mod relation;
mod schema;
pub use active_model::*;
pub use base_entity::*;
@ -15,3 +16,4 @@ pub use model::*;
// pub use prelude::*;
pub use primary_key::*;
pub use relation::*;
pub use schema::*;

View File

@ -1,4 +1,4 @@
use crate::{DbErr, EntityTrait, QueryFilter, QueryResult, Related, Select};
use crate::{DbErr, EntityTrait, Linked, QueryFilter, QueryResult, Related, Select};
pub use sea_query::Value;
use std::fmt::Debug;
@ -16,6 +16,13 @@ pub trait ModelTrait: Clone + Debug {
{
<Self::Entity as Related<R>>::find_related().belongs_to(self)
}
fn find_linked<L>(&self, l: L) -> Select<L::ToEntity>
where
L: Linked<FromEntity = Self::Entity>,
{
l.find_linked()
}
}
pub trait FromQueryResult {

View File

@ -1,9 +1,9 @@
pub use crate::{
error::*, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait, ColumnType,
DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity,
DeriveModel, DerivePrimaryKey, EntityModel, EntityName, EntityTrait, EnumIter, Iden, IdenStatic, ModelTrait,
PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult, Related, RelationDef,
RelationTrait, Select, Value,
DeriveModel, DerivePrimaryKey, EntityModel, EntityName, EntityTrait, EnumIter, ForeignKeyAction, Iden,
IdenStatic, Linked, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult,
Related, RelationDef, RelationTrait, Select, Value,
};
#[cfg(feature = "with-json")]

View File

@ -1,7 +1,18 @@
use super::{ColumnTrait, IdenStatic, Iterable};
use crate::{TryFromU64, TryGetableMany};
use sea_query::IntoValueTuple;
use std::fmt::Debug;
//LINT: composite primary key cannot auto increment
pub trait PrimaryKeyTrait: IdenStatic + Iterable {
type ValueType: Sized
+ Default
+ Debug
+ PartialEq
+ IntoValueTuple
+ TryGetableMany
+ TryFromU64;
fn auto_increment() -> bool;
}

View File

@ -9,6 +9,8 @@ pub enum RelationType {
HasMany,
}
pub type ForeignKeyAction = sea_query::ForeignKeyAction;
pub trait RelationTrait: Iterable + Debug + 'static {
fn def(&self) -> RelationDef;
}
@ -28,14 +30,35 @@ where
}
}
pub trait Linked {
type FromEntity: EntityTrait;
type ToEntity: EntityTrait;
fn link(&self) -> Vec<RelationDef>;
fn find_linked(&self) -> Select<Self::ToEntity> {
let mut select = Select::new();
for rel in self.link().into_iter().rev() {
select = select.join_rev(JoinType::InnerJoin, rel);
}
select
}
}
#[derive(Debug)]
pub struct RelationDef {
pub rel_type: RelationType,
pub from_tbl: TableRef,
pub to_tbl: TableRef,
pub from_col: Identity,
pub to_col: Identity,
pub is_owner: bool,
pub on_delete: Option<ForeignKeyAction>,
pub on_update: Option<ForeignKeyAction>,
}
#[derive(Debug)]
pub struct RelationBuilder<E, R>
where
E: EntityTrait,
@ -47,6 +70,9 @@ where
to_tbl: TableRef,
from_col: Option<Identity>,
to_col: Option<Identity>,
is_owner: bool,
on_delete: Option<ForeignKeyAction>,
on_update: Option<ForeignKeyAction>,
}
impl RelationDef {
@ -58,6 +84,9 @@ impl RelationDef {
to_tbl: self.from_tbl,
from_col: self.to_col,
to_col: self.from_col,
is_owner: !self.is_owner,
on_delete: self.on_delete,
on_update: self.on_update,
}
}
}
@ -67,7 +96,7 @@ where
E: EntityTrait,
R: EntityTrait,
{
pub(crate) fn new(rel_type: RelationType, from: E, to: R) -> Self {
pub(crate) fn new(rel_type: RelationType, from: E, to: R, is_owner: bool) -> Self {
Self {
entities: PhantomData,
rel_type,
@ -75,10 +104,13 @@ where
to_tbl: to.table_ref(),
from_col: None,
to_col: None,
is_owner,
on_delete: None,
on_update: None,
}
}
pub(crate) fn from_rel(rel_type: RelationType, rel: RelationDef) -> Self {
pub(crate) fn from_rel(rel_type: RelationType, rel: RelationDef, is_owner: bool) -> Self {
Self {
entities: PhantomData,
rel_type,
@ -86,6 +118,9 @@ where
to_tbl: rel.to_tbl,
from_col: Some(rel.from_col),
to_col: Some(rel.to_col),
is_owner,
on_delete: None,
on_update: None,
}
}
@ -104,6 +139,16 @@ where
self.to_col = Some(identifier.identity_of());
self
}
pub fn on_delete(mut self, action: ForeignKeyAction) -> Self {
self.on_delete = Some(action);
self
}
pub fn on_update(mut self, action: ForeignKeyAction) -> Self {
self.on_update = Some(action);
self
}
}
impl<E, R> From<RelationBuilder<E, R>> for RelationDef
@ -118,6 +163,9 @@ where
to_tbl: b.to_tbl,
from_col: b.from_col.unwrap(),
to_col: b.to_col.unwrap(),
is_owner: b.is_owner,
on_delete: b.on_delete,
on_update: b.on_update,
}
}
}

161
src/entity/schema.rs Normal file
View File

@ -0,0 +1,161 @@
use crate::{
unpack_table_ref, ColumnTrait, EntityTrait, Identity, Iterable, PrimaryKeyToColumn,
PrimaryKeyTrait, RelationTrait,
};
use sea_query::{ColumnDef, ForeignKeyCreateStatement, Iden, Index, TableCreateStatement};
pub fn entity_to_table_create_statement<E>(entity: E) -> TableCreateStatement
where
E: EntityTrait,
{
let mut stmt = TableCreateStatement::new();
for column in E::Column::iter() {
let orm_column_def = column.def();
let types = orm_column_def.col_type.into();
let mut column_def = ColumnDef::new_with_type(column, types);
if !orm_column_def.null {
column_def.not_null();
}
if orm_column_def.unique {
column_def.unique_key();
}
for primary_key in E::PrimaryKey::iter() {
if column.to_string() == primary_key.into_column().to_string() {
if E::PrimaryKey::auto_increment() {
column_def.auto_increment();
}
if E::PrimaryKey::iter().count() == 1 {
column_def.primary_key();
}
}
}
if orm_column_def.indexed {
stmt.index(
Index::create()
.name(&format!(
"idx-{}-{}",
entity.to_string(),
column.to_string()
))
.table(entity)
.col(column),
);
}
stmt.col(&mut column_def);
}
if E::PrimaryKey::iter().count() > 1 {
let mut idx_pk = Index::create();
for primary_key in E::PrimaryKey::iter() {
idx_pk.col(primary_key);
}
stmt.primary_key(idx_pk.name(&format!("pk-{}", entity.to_string())).primary());
}
for relation in E::Relation::iter() {
let relation = relation.def();
if relation.is_owner {
continue;
}
let mut foreign_key_stmt = ForeignKeyCreateStatement::new();
let from_tbl = unpack_table_ref(&relation.from_tbl);
let to_tbl = unpack_table_ref(&relation.to_tbl);
match relation.from_col {
Identity::Unary(o1) => {
foreign_key_stmt.from_col(o1);
}
Identity::Binary(o1, o2) => {
foreign_key_stmt.from_col(o1);
foreign_key_stmt.from_col(o2);
}
Identity::Ternary(o1, o2, o3) => {
foreign_key_stmt.from_col(o1);
foreign_key_stmt.from_col(o2);
foreign_key_stmt.from_col(o3);
}
}
match relation.to_col {
Identity::Unary(o1) => {
foreign_key_stmt.to_col(o1);
}
Identity::Binary(o1, o2) => {
foreign_key_stmt.to_col(o1);
foreign_key_stmt.to_col(o2);
}
crate::Identity::Ternary(o1, o2, o3) => {
foreign_key_stmt.to_col(o1);
foreign_key_stmt.to_col(o2);
foreign_key_stmt.to_col(o3);
}
}
if let Some(action) = relation.on_delete {
foreign_key_stmt.on_delete(action);
}
if let Some(action) = relation.on_update {
foreign_key_stmt.on_update(action);
}
stmt.foreign_key(
foreign_key_stmt
.name(&format!(
"fk-{}-{}",
from_tbl.to_string(),
to_tbl.to_string()
))
.from_tbl(from_tbl)
.to_tbl(to_tbl),
);
}
stmt.table(entity).if_not_exists().take()
}
#[cfg(test)]
mod tests {
use crate::{entity_to_table_create_statement, tests_cfg::*};
use pretty_assertions::assert_eq;
use sea_query::*;
#[test]
fn test_entity_to_table_create_statement() {
assert_eq!(
entity_to_table_create_statement(CakeFillingPrice).to_string(MysqlQueryBuilder),
Table::create()
.table(CakeFillingPrice)
.if_not_exists()
.col(
ColumnDef::new(cake_filling_price::Column::CakeId)
.integer()
.not_null()
)
.col(
ColumnDef::new(cake_filling_price::Column::FillingId)
.integer()
.not_null()
)
.col(
ColumnDef::new(cake_filling_price::Column::Price)
.decimal()
.not_null()
)
.primary_key(
Index::create()
.name("pk-cake_filling_price")
.col(cake_filling_price::Column::CakeId)
.col(cake_filling_price::Column::FillingId)
.primary()
)
.foreign_key(
ForeignKeyCreateStatement::new()
.name("fk-cake_filling_price-cake_filling")
.from_tbl(CakeFillingPrice)
.from_col(cake_filling_price::Column::CakeId)
.from_col(cake_filling_price::Column::FillingId)
.to_tbl(CakeFilling)
.to_col(cake_filling::Column::CakeId)
.to_col(cake_filling::Column::FillingId)
)
.to_string(MysqlQueryBuilder)
);
}
}

View File

@ -1,4 +1,4 @@
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum DbErr {
Conn(String),
Exec(String),

View File

@ -8,10 +8,7 @@ pub(crate) enum ExecResultHolder {
#[cfg(feature = "sqlx-mysql")]
SqlxMySql(sqlx::mysql::MySqlQueryResult),
#[cfg(feature = "sqlx-postgres")]
SqlxPostgres {
last_insert_id: u64,
rows_affected: u64,
},
SqlxPostgres(sqlx::postgres::PgQueryResult),
#[cfg(feature = "sqlx-sqlite")]
SqlxSqlite(sqlx::sqlite::SqliteQueryResult),
#[cfg(feature = "mock")]
@ -26,7 +23,9 @@ impl ExecResult {
#[cfg(feature = "sqlx-mysql")]
ExecResultHolder::SqlxMySql(result) => result.last_insert_id(),
#[cfg(feature = "sqlx-postgres")]
ExecResultHolder::SqlxPostgres { last_insert_id, .. } => last_insert_id.to_owned(),
ExecResultHolder::SqlxPostgres(_) => {
panic!("Should not retrieve last_insert_id this way")
}
#[cfg(feature = "sqlx-sqlite")]
ExecResultHolder::SqlxSqlite(result) => {
let last_insert_rowid = result.last_insert_rowid();
@ -46,7 +45,7 @@ impl ExecResult {
#[cfg(feature = "sqlx-mysql")]
ExecResultHolder::SqlxMySql(result) => result.rows_affected(),
#[cfg(feature = "sqlx-postgres")]
ExecResultHolder::SqlxPostgres { rows_affected, .. } => rows_affected.to_owned(),
ExecResultHolder::SqlxPostgres(result) => result.rows_affected(),
#[cfg(feature = "sqlx-sqlite")]
ExecResultHolder::SqlxSqlite(result) => result.rows_affected(),
#[cfg(feature = "mock")]

View File

@ -1,15 +1,25 @@
use crate::{error::*, ActiveModelTrait, DatabaseConnection, Insert, Statement};
use crate::{
error::*, ActiveModelTrait, DatabaseConnection, EntityTrait, Insert, PrimaryKeyTrait,
Statement, TryFromU64,
};
use sea_query::InsertStatement;
use std::future::Future;
use std::{future::Future, marker::PhantomData};
#[derive(Clone, Debug)]
pub struct Inserter {
pub struct Inserter<A>
where
A: ActiveModelTrait,
{
query: InsertStatement,
model: PhantomData<A>,
}
#[derive(Clone, Debug)]
pub struct InsertResult {
pub last_insert_id: u64,
#[derive(Debug)]
pub struct InsertResult<A>
where
A: ActiveModelTrait,
{
pub last_insert_id: <<<A as ActiveModelTrait>::Entity as EntityTrait>::PrimaryKey as PrimaryKeyTrait>::ValueType,
}
impl<A> Insert<A>
@ -17,54 +27,79 @@ where
A: ActiveModelTrait,
{
#[allow(unused_mut)]
pub fn exec(
pub fn exec<'a>(
self,
db: &DatabaseConnection,
) -> impl Future<Output = Result<InsertResult, DbErr>> + '_ {
db: &'a DatabaseConnection,
) -> impl Future<Output = Result<InsertResult<A>, DbErr>> + 'a
where
A: 'a,
{
// so that self is dropped before entering await
let mut query = self.query;
#[cfg(feature = "sqlx-postgres")]
if let DatabaseConnection::SqlxPostgresPoolConnection(_) = db {
use crate::{EntityTrait, Iterable};
use sea_query::{Alias, Expr, Query};
for key in <A::Entity as EntityTrait>::PrimaryKey::iter() {
use crate::{sea_query::Query, Iterable};
if <A::Entity as EntityTrait>::PrimaryKey::iter().count() > 0 {
query.returning(
Query::select()
.expr_as(Expr::col(key), Alias::new("last_insert_id"))
.to_owned(),
.columns(<A::Entity as EntityTrait>::PrimaryKey::iter())
.take(),
);
}
}
Inserter::new(query).exec(db)
Inserter::<A>::new(query).exec(db)
}
}
impl Inserter {
impl<A> Inserter<A>
where
A: ActiveModelTrait,
{
pub fn new(query: InsertStatement) -> Self {
Self { query }
Self {
query,
model: PhantomData,
}
}
pub fn exec(
pub fn exec<'a>(
self,
db: &DatabaseConnection,
) -> impl Future<Output = Result<InsertResult, DbErr>> + '_ {
db: &'a DatabaseConnection,
) -> impl Future<Output = Result<InsertResult<A>, DbErr>> + 'a
where
A: 'a,
{
let builder = db.get_database_backend();
exec_insert(builder.build(&self.query), db)
}
}
// Only Statement impl Send
async fn exec_insert(statement: Statement, db: &DatabaseConnection) -> Result<InsertResult, DbErr> {
// TODO: Postgres instead use query_one + returning clause
let result = match db {
async fn exec_insert<A>(
statement: Statement,
db: &DatabaseConnection,
) -> Result<InsertResult<A>, DbErr>
where
A: ActiveModelTrait,
{
type PrimaryKey<A> = <<A as ActiveModelTrait>::Entity as EntityTrait>::PrimaryKey;
type ValueTypeOf<A> = <PrimaryKey<A> as PrimaryKeyTrait>::ValueType;
let last_insert_id = match db {
#[cfg(feature = "sqlx-postgres")]
DatabaseConnection::SqlxPostgresPoolConnection(conn) => {
use crate::{sea_query::Iden, Iterable};
let cols = PrimaryKey::<A>::iter()
.map(|col| col.to_string())
.collect::<Vec<_>>();
let res = conn.query_one(statement).await?.unwrap();
crate::query_result_into_exec_result(res)?
res.try_get_many("", cols.as_ref()).unwrap_or_default()
}
_ => {
let last_insert_id = db.execute(statement).await?.last_insert_id();
ValueTypeOf::<A>::try_from_u64(last_insert_id)
.ok()
.unwrap_or_default()
}
_ => db.execute(statement).await?,
};
Ok(InsertResult {
last_insert_id: result.last_insert_id(),
})
Ok(InsertResult { last_insert_id })
}

View File

@ -17,6 +17,11 @@ pub(crate) enum QueryResultRow {
Mock(crate::MockRow),
}
pub trait TryGetable: Sized {
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, TryGetError>;
}
#[derive(Debug)]
pub enum TryGetError {
DbErr(DbErr),
Null,
@ -31,12 +36,6 @@ impl From<TryGetError> for DbErr {
}
}
pub trait TryGetable {
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, TryGetError>
where
Self: Sized;
}
// QueryResult //
impl QueryResult {
@ -46,6 +45,13 @@ impl QueryResult {
{
Ok(T::try_get(self, pre, col)?)
}
pub fn try_get_many<T>(&self, pre: &str, cols: &[String]) -> Result<T, DbErr>
where
T: TryGetableMany,
{
Ok(T::try_get_many(self, pre, cols)?)
}
}
impl fmt::Debug for QueryResultRow {
@ -54,9 +60,9 @@ impl fmt::Debug for QueryResultRow {
#[cfg(feature = "sqlx-mysql")]
Self::SqlxMySql(row) => write!(f, "{:?}", row),
#[cfg(feature = "sqlx-postgres")]
Self::SqlxPostgres(_) => panic!("QueryResultRow::SqlxPostgres cannot be inspected"),
Self::SqlxPostgres(_) => write!(f, "QueryResultRow::SqlxPostgres cannot be inspected"),
#[cfg(feature = "sqlx-sqlite")]
Self::SqlxSqlite(_) => panic!("QueryResultRow::SqlxSqlite cannot be inspected"),
Self::SqlxSqlite(_) => write!(f, "QueryResultRow::SqlxSqlite cannot be inspected"),
#[cfg(feature = "mock")]
Self::Mock(row) => write!(f, "{:?}", row),
}
@ -103,6 +109,7 @@ macro_rules! try_getable_all {
.and_then(|opt| opt.ok_or(TryGetError::Null))
}
#[cfg(feature = "mock")]
#[allow(unused_variables)]
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
debug_print!("{:#?}", e.to_string());
TryGetError::Null
@ -138,6 +145,7 @@ macro_rules! try_getable_unsigned {
.and_then(|opt| opt.ok_or(TryGetError::Null))
}
#[cfg(feature = "mock")]
#[allow(unused_variables)]
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
debug_print!("{:#?}", e.to_string());
TryGetError::Null
@ -170,6 +178,7 @@ macro_rules! try_getable_mysql {
panic!("{} unsupported by sqlx-sqlite", stringify!($type))
}
#[cfg(feature = "mock")]
#[allow(unused_variables)]
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
debug_print!("{:#?}", e.to_string());
TryGetError::Null
@ -202,6 +211,7 @@ macro_rules! try_getable_postgres {
panic!("{} unsupported by sqlx-sqlite", stringify!($type))
}
#[cfg(feature = "mock")]
#[allow(unused_variables)]
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
debug_print!("{:#?}", e.to_string());
TryGetError::Null
@ -264,12 +274,16 @@ impl TryGetable for Decimal {
.map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e)))?;
use rust_decimal::prelude::FromPrimitive;
match val {
Some(v) => Decimal::from_f64(v)
.ok_or_else(|| TryGetError::DbErr(DbErr::Query("Failed to convert f64 into Decimal".to_owned()))),
None => Err(TryGetError::Null)
Some(v) => Decimal::from_f64(v).ok_or_else(|| {
TryGetError::DbErr(DbErr::Query(
"Failed to convert f64 into Decimal".to_owned(),
))
}),
None => Err(TryGetError::Null),
}
}
#[cfg(feature = "mock")]
#[allow(unused_variables)]
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
debug_print!("{:#?}", e.to_string());
TryGetError::Null
@ -280,3 +294,135 @@ impl TryGetable for Decimal {
#[cfg(feature = "with-uuid")]
try_getable_all!(uuid::Uuid);
// TryGetableMany //
pub trait TryGetableMany: Sized {
fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result<Self, TryGetError>;
}
impl<T> TryGetableMany for T
where
T: TryGetable,
{
fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result<Self, TryGetError> {
try_get_many_with_slice_len_of(1, cols)?;
T::try_get(res, pre, &cols[0])
}
}
impl<T> TryGetableMany for (T, T)
where
T: TryGetable,
{
fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result<Self, TryGetError> {
try_get_many_with_slice_len_of(2, cols)?;
Ok((
T::try_get(res, pre, &cols[0])?,
T::try_get(res, pre, &cols[1])?,
))
}
}
impl<T> TryGetableMany for (T, T, T)
where
T: TryGetable,
{
fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result<Self, TryGetError> {
try_get_many_with_slice_len_of(3, cols)?;
Ok((
T::try_get(res, pre, &cols[0])?,
T::try_get(res, pre, &cols[1])?,
T::try_get(res, pre, &cols[2])?,
))
}
}
fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), TryGetError> {
if cols.len() < len {
Err(TryGetError::DbErr(DbErr::Query(format!(
"Expect {} column names supplied but got slice of length {}",
len,
cols.len()
))))
} else {
Ok(())
}
}
// TryFromU64 //
pub trait TryFromU64: Sized {
fn try_from_u64(n: u64) -> Result<Self, DbErr>;
}
macro_rules! try_from_u64_err {
( $type: ty ) => {
impl TryFromU64 for $type {
fn try_from_u64(_: u64) -> Result<Self, DbErr> {
Err(DbErr::Exec(format!(
"{} cannot be converted from u64",
stringify!($type)
)))
}
}
};
}
macro_rules! try_from_u64_tuple {
( $type: ty ) => {
try_from_u64_err!(($type, $type));
try_from_u64_err!(($type, $type, $type));
};
}
macro_rules! try_from_u64_numeric {
( $type: ty ) => {
impl TryFromU64 for $type {
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
use std::convert::TryInto;
n.try_into().map_err(|_| {
DbErr::Exec(format!(
"fail to convert '{}' into '{}'",
n,
stringify!($type)
))
})
}
}
try_from_u64_tuple!($type);
};
}
try_from_u64_numeric!(i8);
try_from_u64_numeric!(i16);
try_from_u64_numeric!(i32);
try_from_u64_numeric!(i64);
try_from_u64_numeric!(u8);
try_from_u64_numeric!(u16);
try_from_u64_numeric!(u32);
try_from_u64_numeric!(u64);
macro_rules! try_from_u64_string {
( $type: ty ) => {
impl TryFromU64 for $type {
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
Ok(n.to_string())
}
}
try_from_u64_tuple!($type);
};
}
try_from_u64_string!(String);
macro_rules! try_from_u64_dummy {
( $type: ty ) => {
try_from_u64_err!($type);
try_from_u64_err!(($type, $type));
try_from_u64_err!(($type, $type, $type));
};
}
#[cfg(feature = "with-uuid")]
try_from_u64_dummy!(uuid::Uuid);

View File

@ -1,6 +1,6 @@
use crate::{
error::*, query::combine, DatabaseConnection, EntityTrait, FromQueryResult, Iterable,
JsonValue, ModelTrait, Paginator, PrimaryKeyToColumn, QueryResult, Select, SelectTwo,
error::*, DatabaseConnection, EntityTrait, FromQueryResult, IdenStatic, Iterable, JsonValue,
ModelTrait, Paginator, PrimaryKeyToColumn, QueryResult, Select, SelectA, SelectB, SelectTwo,
SelectTwoMany, Statement,
};
use sea_query::SelectStatement;
@ -30,6 +30,7 @@ pub trait SelectorTrait {
fn from_raw_query_result(res: QueryResult) -> Result<Self::Item, DbErr>;
}
#[derive(Debug)]
pub struct SelectModel<M>
where
M: FromQueryResult,
@ -66,8 +67,8 @@ where
fn from_raw_query_result(res: QueryResult) -> Result<Self::Item, DbErr> {
Ok((
M::from_query_result(&res, combine::SELECT_A)?,
N::from_query_result_optional(&res, combine::SELECT_B)?,
M::from_query_result(&res, SelectA.as_str())?,
N::from_query_result_optional(&res, SelectB.as_str())?,
))
}
}
@ -128,7 +129,7 @@ where
E: EntityTrait,
F: EntityTrait,
{
fn into_model<M, N>(self) -> Selector<SelectTwoModel<M, N>>
pub fn into_model<M, N>(self) -> Selector<SelectTwoModel<M, N>>
where
M: FromQueryResult,
N: FromQueryResult,
@ -289,11 +290,12 @@ where
///
/// # let _: Result<(), DbErr> = smol::block_on(async {
/// #
/// let res: Vec<SelectResult> = cake::Entity::find().from_raw_sql(
/// Statement::from_sql_and_values(
/// DbBackend::Postgres, r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, vec![]
/// )
/// )
/// let res: Vec<SelectResult> = cake::Entity::find()
/// .from_raw_sql(Statement::from_sql_and_values(
/// DbBackend::Postgres,
/// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#,
/// vec![],
/// ))
/// .into_model::<SelectResult>()
/// .all(&db)
/// .await?;
@ -317,11 +319,12 @@ where
///
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![
/// Transaction::from_sql_and_values(
/// DbBackend::Postgres, r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, vec![]
/// ),
/// ]);
/// vec![Transaction::from_sql_and_values(
/// DbBackend::Postgres,
/// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#,
/// vec![]
/// ),]
/// );
/// ```
pub fn into_model<M>(self) -> SelectorRaw<SelectModel<M>>
where
@ -406,22 +409,26 @@ where
///
/// # let _: Result<(), DbErr> = smol::block_on(async {
/// #
/// let _: Option<cake::Model> = cake::Entity::find().from_raw_sql(
/// Statement::from_sql_and_values(
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, vec![1.into()]
/// )
/// ).one(&db).await?;
/// let _: Option<cake::Model> = cake::Entity::find()
/// .from_raw_sql(Statement::from_sql_and_values(
/// DbBackend::Postgres,
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#,
/// vec![1.into()],
/// ))
/// .one(&db)
/// .await?;
/// #
/// # Ok(())
/// # });
///
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![
/// Transaction::from_sql_and_values(
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, vec![1.into()]
/// ),
/// ]);
/// vec![Transaction::from_sql_and_values(
/// DbBackend::Postgres,
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#,
/// vec![1.into()]
/// ),]
/// );
/// ```
pub async fn one(self, db: &DatabaseConnection) -> Result<Option<S::Item>, DbErr> {
let row = db.query_one(self.stmt).await?;
@ -441,22 +448,26 @@ where
///
/// # let _: Result<(), DbErr> = smol::block_on(async {
/// #
/// let _: Vec<cake::Model> = cake::Entity::find().from_raw_sql(
/// Statement::from_sql_and_values(
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![]
/// )
/// ).all(&db).await?;
/// let _: Vec<cake::Model> = cake::Entity::find()
/// .from_raw_sql(Statement::from_sql_and_values(
/// DbBackend::Postgres,
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake""#,
/// vec![],
/// ))
/// .all(&db)
/// .await?;
/// #
/// # Ok(())
/// # });
///
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![
/// Transaction::from_sql_and_values(
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![]
/// ),
/// ]);
/// vec![Transaction::from_sql_and_values(
/// DbBackend::Postgres,
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake""#,
/// vec![]
/// ),]
/// );
/// ```
pub async fn all(self, db: &DatabaseConnection) -> Result<Vec<S::Item>, DbErr> {
let rows = db.query_all(self.stmt).await?;

View File

@ -1,3 +1,10 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(
missing_debug_implementations,
clippy::print_stderr,
clippy::print_stdout
)]
//! <div align="center">
//!
//! <img src="https://www.sea-ql.org/SeaORM/img/SeaORM banner.png"/>
@ -20,13 +27,9 @@
//!
//! SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust.
//!
//! ```markdown
//! This is an early release of SeaORM, the API is not stable yet.
//! ```
//!
//! [![Getting Started](https://img.shields.io/badge/Getting%20Started-blue)](https://www.sea-ql.org/SeaORM/docs/index)
//! [![Examples](https://img.shields.io/badge/Examples-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/sqlx)
//! [![Starter Kit](https://img.shields.io/badge/Starter%20Kit-green)](https://github.com/SeaQL/sea-orm/issues/37)
//! [![Examples](https://img.shields.io/badge/Examples-green)](https://github.com/SeaQL/sea-orm/tree/master/examples)
//! [![Rocket Example](https://img.shields.io/badge/Rocket%20Example-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example)
//! [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv)
//!
//! ## Features
@ -70,10 +73,8 @@
//! 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?;
//! let cake_with_fruits: Vec<(cake::Model, Vec<fruit::Model>)> =
//! Cake::find().find_with_related(Fruit).all(db).await?;
//!
//! # Ok(())
//! # }
@ -93,7 +94,7 @@
//! };
//!
//! // insert one
//! let res: InsertResult = Fruit::insert(pear).exec(db).await?;
//! let res = Fruit::insert(pear).exec(db).await?;
//!
//! println!("InsertResult: {}", res.last_insert_id);
//! # Ok(())
@ -180,6 +181,13 @@
//! # Ok(())
//! # }
//! ```
//!
//! ## Learn More
//!
//! 1. [Design](https://github.com/SeaQL/sea-orm/tree/master/DESIGN.md)
//! 1. [Architecture](https://github.com/SeaQL/sea-orm/tree/master/ARCHITECTURE.md)
//! 1. [Compare with Diesel](https://www.sea-ql.org/SeaORM/docs/internal-design/diesel)
//!
//! ## License
//!
//! Licensed under either of

View File

@ -1,10 +1,31 @@
use crate::{EntityTrait, IntoSimpleExpr, Iterable, QueryTrait, Select, SelectTwo, SelectTwoMany};
use crate::{
EntityTrait, IdenStatic, IntoSimpleExpr, Iterable, QueryTrait, Select, SelectTwo, SelectTwoMany,
};
use core::marker::PhantomData;
pub use sea_query::JoinType;
use sea_query::{Alias, ColumnRef, Iden, Order, SeaRc, SelectExpr, SelectStatement, SimpleExpr};
pub const SELECT_A: &str = "A_";
pub const SELECT_B: &str = "B_";
macro_rules! select_def {
( $ident: ident, $str: expr ) => {
#[derive(Debug, Clone, Copy)]
pub struct $ident;
impl Iden for $ident {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(s, "{}", self.as_str()).unwrap();
}
}
impl IdenStatic for $ident {
fn as_str(&self) -> &str {
$str
}
}
};
}
select_def!(SelectA, "A_");
select_def!(SelectB, "B_");
impl<E> Select<E>
where
@ -37,7 +58,7 @@ where
where
F: EntityTrait,
{
self = self.apply_alias(SELECT_A);
self = self.apply_alias(SelectA.as_str());
SelectTwo::new(self.into_query())
}
@ -45,7 +66,7 @@ where
where
F: EntityTrait,
{
self = self.apply_alias(SELECT_A);
self = self.apply_alias(SelectA.as_str());
SelectTwoMany::new(self.into_query())
}
}
@ -102,7 +123,7 @@ where
S: QueryTrait<QueryStatement = SelectStatement>,
{
for col in <F::Column as Iterable>::iter() {
let alias = format!("{}{}", SELECT_B, col.to_string().as_str());
let alias = format!("{}{}", SelectB.as_str(), col.as_str());
selector.query().expr(SelectExpr {
expr: col.into_simple_expr(),
alias: Some(SeaRc::new(Alias::new(&alias))),

View File

@ -1,11 +1,9 @@
use crate::{
ColumnTrait, EntityTrait, Identity, IntoSimpleExpr, Iterable, ModelTrait, PrimaryKeyToColumn,
RelationDef,
};
use sea_query::{
Alias, Expr, IntoCondition, SeaRc, SelectExpr, SelectStatement, SimpleExpr, TableRef,
ColumnTrait, EntityTrait, Identity, IntoIdentity, IntoSimpleExpr, Iterable, ModelTrait,
PrimaryKeyToColumn, RelationDef,
};
pub use sea_query::{Condition, ConditionalStatement, DynIden, JoinType, Order, OrderedStatement};
use sea_query::{Expr, IntoCondition, SeaRc, SelectExpr, SelectStatement, SimpleExpr, TableRef};
// LINT: when the column does not appear in tables selected from
// LINT: when there is a group by clause, but some columns don't have aggregate functions
@ -55,13 +53,14 @@ pub trait QuerySelect: Sized {
/// r#"SELECT COUNT("cake"."id") AS "count" FROM "cake""#
/// );
/// ```
fn column_as<C>(mut self, col: C, alias: &str) -> Self
fn column_as<C, I>(mut self, col: C, alias: I) -> Self
where
C: IntoSimpleExpr,
I: IntoIdentity,
{
self.query().expr(SelectExpr {
expr: col.into_simple_expr(),
alias: Some(SeaRc::new(Alias::new(alias))),
alias: Some(SeaRc::new(alias.into_identity())),
});
self
}
@ -295,7 +294,7 @@ fn join_condition(rel: RelationDef) -> SimpleExpr {
}
}
fn unpack_table_ref(table_ref: &TableRef) -> DynIden {
pub(crate) fn unpack_table_ref(table_ref: &TableRef) -> DynIden {
match table_ref {
TableRef::Table(tbl) => SeaRc::clone(tbl),
TableRef::SchemaTable(_, tbl) => SeaRc::clone(tbl),

View File

@ -1,4 +1,4 @@
use crate::{EntityTrait, QuerySelect, Related, Select, SelectTwo, SelectTwoMany};
use crate::{EntityTrait, Linked, QuerySelect, Related, Select, SelectTwo, SelectTwoMany};
pub use sea_query::JoinType;
impl<E> Select<E>
@ -57,6 +57,19 @@ where
{
self.left_join(r).select_with(r)
}
/// Left Join with a Linked Entity and select both Entity.
pub fn find_also_linked<L, T>(self, l: L) -> SelectTwo<E, T>
where
L: Linked<FromEntity = E, ToEntity = T>,
T: EntityTrait,
{
let mut slf = self;
for rel in l.link() {
slf = slf.join(JoinType::LeftJoin, rel);
}
slf.select_also(T::default())
}
}
#[cfg(test)]
@ -220,4 +233,44 @@ mod tests {
.join(" ")
);
}
#[test]
fn join_10() {
let cake_model = cake::Model {
id: 12,
name: "".to_owned(),
};
assert_eq!(
cake_model
.find_linked(cake::CakeToFilling)
.build(DbBackend::MySql)
.to_string(),
[
r#"SELECT `filling`.`id`, `filling`.`name`"#,
r#"FROM `filling`"#,
r#"INNER JOIN `cake_filling` ON `cake_filling`.`filling_id` = `filling`.`id`"#,
r#"INNER JOIN `cake` ON `cake`.`id` = `cake_filling`.`cake_id`"#,
]
.join(" ")
);
}
#[test]
fn join_11() {
assert_eq!(
cake::Entity::find()
.find_also_linked(cake::CakeToFilling)
.build(DbBackend::MySql)
.to_string(),
[
r#"SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`,"#,
r#"`filling`.`id` AS `B_id`, `filling`.`name` AS `B_name`"#,
r#"FROM `cake`"#,
r#"LEFT JOIN `cake_filling` ON `cake`.`id` = `cake_filling`.`cake_id`"#,
r#"LEFT JOIN `filling` ON `cake_filling`.`filling_id` = `filling`.`id`"#,
]
.join(" ")
);
}
}

View File

@ -9,7 +9,7 @@ mod select;
mod traits;
mod update;
// pub use combine::*;
pub use combine::{SelectA, SelectB};
pub use delete::*;
pub use helper::*;
pub use insert::*;

View File

@ -59,7 +59,7 @@ impl Update {
/// Update many ActiveModel
///
/// ```
/// use sea_orm::{entity::*, query::*, tests_cfg::fruit, sea_query::Expr, DbBackend};
/// use sea_orm::{entity::*, query::*, sea_query::Expr, tests_cfg::fruit, DbBackend};
///
/// assert_eq!(
/// Update::many(fruit::Entity)

View File

@ -28,6 +28,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
@ -73,4 +75,20 @@ impl Related<super::filling::Entity> for Entity {
}
}
#[derive(Debug)]
pub struct CakeToFilling;
impl Linked for CakeToFilling {
type FromEntity = Entity;
type ToEntity = super::filling::Entity;
fn link(&self) -> Vec<RelationDef> {
vec![
super::cake_filling::Relation::Cake.def().rev(),
super::cake_filling::Relation::Filling.def(),
]
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -29,6 +29,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = (i32, i32);
fn auto_increment() -> bool {
false
}

View File

@ -35,6 +35,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = (i32, i32);
fn auto_increment() -> bool {
false
}

View File

@ -41,6 +41,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}

View File

@ -30,6 +30,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}

View File

@ -1,7 +1,7 @@
#[macro_export]
#[cfg(feature = "debug-print")]
macro_rules! debug_print {
($( $args:expr ),*) => { println!( $( $args ),* ); }
($( $args:expr ),*) => { log::debug!( $( $args ),* ); }
}
#[macro_export]

View File

@ -1,7 +1,6 @@
pub mod common;
#[allow(unused_imports)]
use sea_orm::{entity::*, error::*, sea_query, tests_cfg::*, Database, DbConn};
pub use sea_orm::{entity::*, error::*, sea_query, tests_cfg::*, Database, DbConn};
// DATABASE_URL="sqlite::memory:" cargo test --features sqlx-sqlit,runtime-async-std --test basic
#[sea_orm_macros::test]

View File

@ -31,6 +31,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
@ -49,7 +51,7 @@ impl ColumnTrait for Column {
Self::Id => ColumnType::Integer.def(),
Self::Name => ColumnType::String(None).def(),
Self::ContactDetails => ColumnType::Json.def(),
Self::BakeryId => ColumnType::Integer.def(),
Self::BakeryId => ColumnType::Integer.def().null(),
}
}
}
@ -60,6 +62,8 @@ impl RelationTrait for Relation {
Self::Bakery => Entity::belongs_to(super::bakery::Entity)
.from(Column::BakeryId)
.to(super::bakery::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
.into(),
}
}
@ -81,4 +85,22 @@ impl Related<super::cake::Entity> for Entity {
}
}
pub struct BakedForCustomer;
impl Linked for BakedForCustomer {
type FromEntity = Entity;
type ToEntity = super::customer::Entity;
fn link(&self) -> Vec<RelationDef> {
vec![
super::cakes_bakers::Relation::Baker.def().rev(),
super::cakes_bakers::Relation::Cake.def(),
super::lineitem::Relation::Cake.def().rev(),
super::lineitem::Relation::Order.def(),
super::order::Relation::Customer.def(),
]
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -29,6 +29,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
@ -48,7 +50,7 @@ impl ColumnTrait for Column {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Name => ColumnType::String(None).def(),
Self::ProfitMargin => ColumnType::Float.def(),
Self::ProfitMargin => ColumnType::Double.def(),
}
}
}

View File

@ -35,6 +35,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
@ -54,9 +56,9 @@ impl ColumnTrait for Column {
Self::Id => ColumnType::Integer.def(),
Self::Name => ColumnType::String(None).def(),
Self::Price => ColumnType::Decimal(Some((19, 4))).def(),
Self::BakeryId => ColumnType::Integer.def(),
Self::BakeryId => ColumnType::Integer.def().null(),
Self::GlutenFree => ColumnType::Boolean.def(),
Self::Serial => ColumnType::String(None).def(),
Self::Serial => ColumnType::Uuid.def(),
}
}
}
@ -67,6 +69,8 @@ impl RelationTrait for Relation {
Self::Bakery => Entity::belongs_to(super::bakery::Entity)
.from(Column::BakeryId)
.to(super::bakery::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
.into(),
Self::Lineitem => Entity::has_many(super::lineitem::Entity).into(),
}

View File

@ -28,6 +28,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = (i32, i32);
fn auto_increment() -> bool {
false
}
@ -56,10 +58,14 @@ impl RelationTrait for Relation {
Self::Cake => Entity::belongs_to(super::cake::Entity)
.from(Column::CakeId)
.to(super::cake::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
.into(),
Self::Baker => Entity::belongs_to(super::baker::Entity)
.from(Column::BakerId)
.to(super::baker::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
.into(),
}
}

View File

@ -29,6 +29,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
@ -46,7 +48,7 @@ impl ColumnTrait for Column {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Name => ColumnType::String(None).def(),
Self::Notes => ColumnType::Text.def(),
Self::Notes => ColumnType::Text.def().null(),
}
}
}

View File

@ -33,6 +33,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
@ -64,10 +66,14 @@ impl RelationTrait for Relation {
Self::Order => Entity::belongs_to(super::order::Entity)
.from(Column::OrderId)
.to(super::order::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
.into(),
Self::Cake => Entity::belongs_to(super::cake::Entity)
.from(Column::CakeId)
.to(super::cake::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
.into(),
}
}

View File

@ -0,0 +1,61 @@
use sea_orm::entity::prelude::*;
use uuid::Uuid;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl EntityName for Entity {
fn table_name(&self) -> &str {
"metadata"
}
}
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
pub uuid: Uuid,
pub key: String,
pub value: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Uuid,
Key,
Value,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Uuid,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = Uuid;
fn auto_increment() -> bool {
false
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Uuid => ColumnType::Uuid.def(),
Self::Key => ColumnType::String(None).def(),
Self::Value => ColumnType::String(None).def(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
unreachable!()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -4,6 +4,7 @@ pub mod cake;
pub mod cakes_bakers;
pub mod customer;
pub mod lineitem;
pub mod metadata;
pub mod order;
pub use super::baker::Entity as Baker;
@ -12,4 +13,5 @@ pub use super::cake::Entity as Cake;
pub use super::cakes_bakers::Entity as CakesBakers;
pub use super::customer::Entity as Customer;
pub use super::lineitem::Entity as Lineitem;
pub use super::metadata::Entity as Metadata;
pub use super::order::Entity as Order;

View File

@ -33,6 +33,8 @@ pub enum PrimaryKey {
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
@ -65,10 +67,14 @@ impl RelationTrait for Relation {
Self::Bakery => Entity::belongs_to(super::bakery::Entity)
.from(Column::BakeryId)
.to(super::bakery::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
.into(),
Self::Customer => Entity::belongs_to(super::customer::Entity)
.from(Column::CustomerId)
.to(super::customer::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
.into(),
Self::Lineitem => Entity::has_many(super::lineitem::Entity).into(),
}

View File

@ -19,7 +19,7 @@ impl TestContext {
let db: DatabaseConnection = setup::setup(&base_url, test_name).await;
Self {
base_url: base_url,
base_url,
db_name: test_name.to_string(),
db,
}

View File

@ -45,13 +45,14 @@ pub async fn setup(base_url: &str, db_name: &str) -> DatabaseConnection {
Database::connect(base_url).await.unwrap()
};
assert!(schema::create_bakery_table(&db).await.is_ok());
assert!(schema::create_baker_table(&db).await.is_ok());
assert!(schema::create_customer_table(&db).await.is_ok());
assert!(schema::create_order_table(&db).await.is_ok());
assert!(schema::create_cake_table(&db).await.is_ok());
assert!(schema::create_cakes_bakers_table(&db).await.is_ok());
assert!(schema::create_lineitem_table(&db).await.is_ok());
schema::create_bakery_table(&db).await.unwrap();
schema::create_baker_table(&db).await.unwrap();
schema::create_customer_table(&db).await.unwrap();
schema::create_order_table(&db).await.unwrap();
schema::create_cake_table(&db).await.unwrap();
schema::create_cakes_bakers_table(&db).await.unwrap();
schema::create_lineitem_table(&db).await.unwrap();
schema::create_metadata_table(&db).await.unwrap();
db
}

View File

@ -1,15 +1,29 @@
use sea_orm::{error::*, sea_query, DbConn, ExecResult};
use sea_query::{ColumnDef, ForeignKey, ForeignKeyAction, Index, TableCreateStatement};
pub use super::super::bakery_chain::*;
use pretty_assertions::assert_eq;
use sea_orm::{
entity_to_table_create_statement, error::*, sea_query, DbConn, EntityTrait, ExecResult,
};
use sea_query::{ColumnDef, ForeignKey, ForeignKeyAction, Index, Table, TableCreateStatement};
async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result<ExecResult, DbErr> {
async fn create_table<E>(
db: &DbConn,
stmt: &TableCreateStatement,
entity: E,
) -> Result<ExecResult, DbErr>
where
E: EntityTrait,
{
let builder = db.get_database_backend();
db.execute(builder.build(stmt)).await
let stmt = builder.build(stmt);
assert_eq!(
builder.build(&entity_to_table_create_statement(entity)),
stmt
);
db.execute(stmt).await
}
pub async fn create_bakery_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
let stmt = Table::create()
.table(bakery::Entity)
.if_not_exists()
.col(
@ -27,16 +41,17 @@ pub async fn create_bakery_table(db: &DbConn) -> Result<ExecResult, DbErr> {
)
.to_owned();
create_table(db, &stmt).await
create_table(db, &stmt, Bakery).await
}
pub async fn create_baker_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
let stmt = Table::create()
.table(baker::Entity)
.if_not_exists()
.col(
ColumnDef::new(baker::Column::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
@ -49,7 +64,7 @@ pub async fn create_baker_table(db: &DbConn) -> Result<ExecResult, DbErr> {
.col(ColumnDef::new(baker::Column::BakeryId).integer())
.foreign_key(
ForeignKey::create()
.name("FK_baker_bakery")
.name("fk-baker-bakery")
.from(baker::Entity, baker::Column::BakeryId)
.to(bakery::Entity, bakery::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
@ -57,11 +72,11 @@ pub async fn create_baker_table(db: &DbConn) -> Result<ExecResult, DbErr> {
)
.to_owned();
create_table(db, &stmt).await
create_table(db, &stmt, Baker).await
}
pub async fn create_customer_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
let stmt = Table::create()
.table(customer::Entity)
.if_not_exists()
.col(
@ -75,11 +90,11 @@ pub async fn create_customer_table(db: &DbConn) -> Result<ExecResult, DbErr> {
.col(ColumnDef::new(customer::Column::Notes).text())
.to_owned();
create_table(db, &stmt).await
create_table(db, &stmt, Customer).await
}
pub async fn create_order_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
let stmt = Table::create()
.table(order::Entity)
.if_not_exists()
.col(
@ -107,7 +122,7 @@ pub async fn create_order_table(db: &DbConn) -> Result<ExecResult, DbErr> {
)
.foreign_key(
ForeignKey::create()
.name("FK_order_bakery")
.name("fk-order-bakery")
.from(order::Entity, order::Column::BakeryId)
.to(bakery::Entity, bakery::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
@ -115,7 +130,7 @@ pub async fn create_order_table(db: &DbConn) -> Result<ExecResult, DbErr> {
)
.foreign_key(
ForeignKey::create()
.name("FK_order_customer")
.name("fk-order-customer")
.from(order::Entity, order::Column::CustomerId)
.to(customer::Entity, customer::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
@ -123,11 +138,11 @@ pub async fn create_order_table(db: &DbConn) -> Result<ExecResult, DbErr> {
)
.to_owned();
create_table(db, &stmt).await
create_table(db, &stmt, Order).await
}
pub async fn create_lineitem_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
let stmt = Table::create()
.table(lineitem::Entity)
.if_not_exists()
.col(
@ -159,7 +174,7 @@ pub async fn create_lineitem_table(db: &DbConn) -> Result<ExecResult, DbErr> {
)
.foreign_key(
ForeignKey::create()
.name("FK_lineitem_order")
.name("fk-lineitem-order")
.from(lineitem::Entity, lineitem::Column::OrderId)
.to(order::Entity, order::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
@ -167,7 +182,7 @@ pub async fn create_lineitem_table(db: &DbConn) -> Result<ExecResult, DbErr> {
)
.foreign_key(
ForeignKey::create()
.name("FK_lineitem_cake")
.name("fk-lineitem-cake")
.from(lineitem::Entity, lineitem::Column::CakeId)
.to(cake::Entity, cake::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
@ -175,11 +190,11 @@ pub async fn create_lineitem_table(db: &DbConn) -> Result<ExecResult, DbErr> {
)
.to_owned();
create_table(db, &stmt).await
create_table(db, &stmt, Lineitem).await
}
pub async fn create_cakes_bakers_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
let stmt = Table::create()
.table(cakes_bakers::Entity)
.if_not_exists()
.col(
@ -194,12 +209,13 @@ pub async fn create_cakes_bakers_table(db: &DbConn) -> Result<ExecResult, DbErr>
)
.primary_key(
Index::create()
.name("pk-cakes_bakers")
.col(cakes_bakers::Column::CakeId)
.col(cakes_bakers::Column::BakerId),
)
.foreign_key(
ForeignKey::create()
.name("FK_cakes_bakers_cake")
.name("fk-cakes_bakers-cake")
.from(cakes_bakers::Entity, cakes_bakers::Column::CakeId)
.to(cake::Entity, cake::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
@ -207,7 +223,7 @@ pub async fn create_cakes_bakers_table(db: &DbConn) -> Result<ExecResult, DbErr>
)
.foreign_key(
ForeignKey::create()
.name("FK_cakes_bakers_baker")
.name("fk-cakes_bakers-baker")
.from(cakes_bakers::Entity, cakes_bakers::Column::BakerId)
.to(baker::Entity, baker::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
@ -215,11 +231,11 @@ pub async fn create_cakes_bakers_table(db: &DbConn) -> Result<ExecResult, DbErr>
)
.to_owned();
create_table(db, &stmt).await
create_table(db, &stmt, CakesBakers).await
}
pub async fn create_cake_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
let stmt = Table::create()
.table(cake::Entity)
.if_not_exists()
.col(
@ -238,7 +254,7 @@ pub async fn create_cake_table(db: &DbConn) -> Result<ExecResult, DbErr> {
.col(ColumnDef::new(cake::Column::BakeryId).integer())
.foreign_key(
ForeignKey::create()
.name("FK_cake_bakery")
.name("fk-cake-bakery")
.from(cake::Entity, cake::Column::BakeryId)
.to(bakery::Entity, bakery::Column::Id)
.on_delete(ForeignKeyAction::Cascade)
@ -252,5 +268,22 @@ pub async fn create_cake_table(db: &DbConn) -> Result<ExecResult, DbErr> {
.col(ColumnDef::new(cake::Column::Serial).uuid().not_null())
.to_owned();
create_table(db, &stmt).await
create_table(db, &stmt, Cake).await
}
pub async fn create_metadata_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
.table(metadata::Entity)
.if_not_exists()
.col(
ColumnDef::new(metadata::Column::Uuid)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(metadata::Column::Key).string().not_null())
.col(ColumnDef::new(metadata::Column::Value).string().not_null())
.to_owned();
create_table(db, &stmt, Metadata).await
}

View File

@ -7,7 +7,7 @@ pub async fn test_create_baker(db: &DbConn) {
profit_margin: Set(10.4),
..Default::default()
};
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
let bakery_insert_res = Bakery::insert(seaside_bakery)
.exec(db)
.await
.expect("could not insert bakery");
@ -30,7 +30,7 @@ pub async fn test_create_baker(db: &DbConn) {
bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)),
..Default::default()
};
let res: InsertResult = Baker::insert(baker_bob)
let res = Baker::insert(baker_bob)
.exec(db)
.await
.expect("could not insert baker");

View File

@ -8,7 +8,7 @@ pub async fn test_create_cake(db: &DbConn) {
profit_margin: Set(10.4),
..Default::default()
};
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
let bakery_insert_res = Bakery::insert(seaside_bakery)
.exec(db)
.await
.expect("could not insert bakery");
@ -23,7 +23,7 @@ pub async fn test_create_cake(db: &DbConn) {
bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)),
..Default::default()
};
let baker_insert_res: InsertResult = Baker::insert(baker_bob)
let baker_insert_res = Baker::insert(baker_bob)
.exec(db)
.await
.expect("could not insert baker");
@ -38,7 +38,7 @@ pub async fn test_create_cake(db: &DbConn) {
..Default::default()
};
let cake_insert_res: InsertResult = Cake::insert(mud_cake)
let cake_insert_res = Cake::insert(mud_cake)
.exec(db)
.await
.expect("could not insert cake");
@ -51,18 +51,25 @@ pub async fn test_create_cake(db: &DbConn) {
let cake_baker = cakes_bakers::ActiveModel {
cake_id: Set(cake_insert_res.last_insert_id as i32),
baker_id: Set(baker_insert_res.last_insert_id as i32),
..Default::default()
};
let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker)
let cake_baker_res = CakesBakers::insert(cake_baker.clone())
.exec(db)
.await
.expect("could not insert cake_baker");
assert_eq!(
cake_baker_res.last_insert_id,
if cfg!(feature = "sqlx-postgres") {
(cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap())
} else {
Default::default()
}
);
assert!(cake.is_some());
let cake_model = cake.unwrap();
assert_eq!(cake_model.name, "Mud Cake");
assert_eq!(cake_model.price, dec!(10.25));
assert_eq!(cake_model.gluten_free, false);
assert!(!cake_model.gluten_free);
assert_eq!(
cake_model
.find_related(Bakery)

View File

@ -10,7 +10,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
profit_margin: Set(10.4),
..Default::default()
};
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
let bakery_insert_res = Bakery::insert(seaside_bakery)
.exec(db)
.await
.expect("could not insert bakery");
@ -26,7 +26,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)),
..Default::default()
};
let baker_insert_res: InsertResult = Baker::insert(baker_bob)
let baker_insert_res = Baker::insert(baker_bob)
.exec(db)
.await
.expect("could not insert baker");
@ -41,7 +41,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
..Default::default()
};
let cake_insert_res: InsertResult = Cake::insert(mud_cake)
let cake_insert_res = Cake::insert(mud_cake)
.exec(db)
.await
.expect("could not insert cake");
@ -50,12 +50,19 @@ pub async fn test_create_lineitem(db: &DbConn) {
let cake_baker = cakes_bakers::ActiveModel {
cake_id: Set(cake_insert_res.last_insert_id as i32),
baker_id: Set(baker_insert_res.last_insert_id as i32),
..Default::default()
};
let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker)
let cake_baker_res = CakesBakers::insert(cake_baker.clone())
.exec(db)
.await
.expect("could not insert cake_baker");
assert_eq!(
cake_baker_res.last_insert_id,
if cfg!(feature = "sqlx-postgres") {
(cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap())
} else {
Default::default()
}
);
// Customer
let customer_kate = customer::ActiveModel {
@ -63,7 +70,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
notes: Set(Some("Loves cheese cake".to_owned())),
..Default::default()
};
let customer_insert_res: InsertResult = Customer::insert(customer_kate)
let customer_insert_res = Customer::insert(customer_kate)
.exec(db)
.await
.expect("could not insert customer");
@ -76,7 +83,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
placed_at: Set(Utc::now().naive_utc()),
..Default::default()
};
let order_insert_res: InsertResult = Order::insert(order_1)
let order_insert_res = Order::insert(order_1)
.exec(db)
.await
.expect("could not insert order");
@ -89,7 +96,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
quantity: Set(1),
..Default::default()
};
let lineitem_insert_res: InsertResult = Lineitem::insert(lineitem_1)
let lineitem_insert_res = Lineitem::insert(lineitem_1)
.exec(db)
.await
.expect("could not insert lineitem");
@ -105,7 +112,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
assert_eq!(lineitem_model.price, dec!(7.55));
let cake: Option<cake::Model> = Cake::find_by_id(lineitem_model.cake_id as u64)
let cake: Option<cake::Model> = Cake::find_by_id(lineitem_model.cake_id)
.one(db)
.await
.expect("could not find cake");

View File

@ -10,7 +10,7 @@ pub async fn test_create_order(db: &DbConn) {
profit_margin: Set(10.4),
..Default::default()
};
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
let bakery_insert_res = Bakery::insert(seaside_bakery)
.exec(db)
.await
.expect("could not insert bakery");
@ -26,7 +26,7 @@ pub async fn test_create_order(db: &DbConn) {
bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)),
..Default::default()
};
let baker_insert_res: InsertResult = Baker::insert(baker_bob)
let baker_insert_res = Baker::insert(baker_bob)
.exec(db)
.await
.expect("could not insert baker");
@ -41,7 +41,7 @@ pub async fn test_create_order(db: &DbConn) {
..Default::default()
};
let cake_insert_res: InsertResult = Cake::insert(mud_cake)
let cake_insert_res = Cake::insert(mud_cake)
.exec(db)
.await
.expect("could not insert cake");
@ -50,12 +50,19 @@ pub async fn test_create_order(db: &DbConn) {
let cake_baker = cakes_bakers::ActiveModel {
cake_id: Set(cake_insert_res.last_insert_id as i32),
baker_id: Set(baker_insert_res.last_insert_id as i32),
..Default::default()
};
let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker)
let cake_baker_res = CakesBakers::insert(cake_baker.clone())
.exec(db)
.await
.expect("could not insert cake_baker");
assert_eq!(
cake_baker_res.last_insert_id,
if cfg!(feature = "sqlx-postgres") {
(cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap())
} else {
Default::default()
}
);
// Customer
let customer_kate = customer::ActiveModel {
@ -63,7 +70,7 @@ pub async fn test_create_order(db: &DbConn) {
notes: Set(Some("Loves cheese cake".to_owned())),
..Default::default()
};
let customer_insert_res: InsertResult = Customer::insert(customer_kate)
let customer_insert_res = Customer::insert(customer_kate)
.exec(db)
.await
.expect("could not insert customer");
@ -76,7 +83,7 @@ pub async fn test_create_order(db: &DbConn) {
placed_at: Set(Utc::now().naive_utc()),
..Default::default()
};
let order_insert_res: InsertResult = Order::insert(order_1)
let order_insert_res = Order::insert(order_1)
.exec(db)
.await
.expect("could not insert order");
@ -89,7 +96,7 @@ pub async fn test_create_order(db: &DbConn) {
quantity: Set(2),
..Default::default()
};
let _lineitem_insert_res: InsertResult = Lineitem::insert(lineitem_1)
let _lineitem_insert_res = Lineitem::insert(lineitem_1)
.exec(db)
.await
.expect("could not insert lineitem");
@ -103,7 +110,7 @@ pub async fn test_create_order(db: &DbConn) {
let order_model = order.unwrap();
assert_eq!(order_model.total, dec!(15.10));
let customer: Option<customer::Model> = Customer::find_by_id(order_model.customer_id as u64)
let customer: Option<customer::Model> = Customer::find_by_id(order_model.customer_id)
.one(db)
.await
.expect("could not find customer");
@ -111,7 +118,7 @@ pub async fn test_create_order(db: &DbConn) {
let customer_model = customer.unwrap();
assert_eq!(customer_model.name, "Kate");
let bakery: Option<bakery::Model> = Bakery::find_by_id(order_model.bakery_id as i64)
let bakery: Option<bakery::Model> = Bakery::find_by_id(order_model.bakery_id)
.one(db)
.await
.expect("could not find bakery");

View File

@ -10,7 +10,7 @@ pub async fn test_delete_cake(db: &DbConn) {
profit_margin: Set(10.4),
..Default::default()
};
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
let bakery_insert_res = Bakery::insert(seaside_bakery)
.exec(db)
.await
.expect("could not insert bakery");

View File

@ -1,7 +1,3 @@
use sea_orm::{entity::*, DbConn, InsertResult};
pub use super::common::bakery_chain::*;
pub mod create_baker;
pub mod create_cake;
pub mod create_lineitem;
@ -9,13 +5,23 @@ pub mod create_order;
pub mod deletes;
pub mod updates;
pub use create_baker::*;
pub use create_cake::*;
pub use create_lineitem::*;
pub use create_order::*;
pub use deletes::*;
pub use updates::*;
pub use super::common::bakery_chain::*;
use sea_orm::{entity::*, DbConn};
pub async fn test_create_bakery(db: &DbConn) {
let seaside_bakery = bakery::ActiveModel {
name: Set("SeaSide Bakery".to_owned()),
profit_margin: Set(10.4),
..Default::default()
};
let res: InsertResult = Bakery::insert(seaside_bakery)
let res = Bakery::insert(seaside_bakery)
.exec(db)
.await
.expect("could not insert bakery");
@ -28,7 +34,7 @@ pub async fn test_create_bakery(db: &DbConn) {
assert!(bakery.is_some());
let bakery_model = bakery.unwrap();
assert_eq!(bakery_model.name, "SeaSide Bakery");
assert_eq!(bakery_model.profit_margin, 10.4);
assert!((bakery_model.profit_margin - 10.4).abs() < f64::EPSILON);
}
pub async fn test_create_customer(db: &DbConn) {
@ -37,7 +43,7 @@ pub async fn test_create_customer(db: &DbConn) {
notes: Set(Some("Loves cheese cake".to_owned())),
..Default::default()
};
let res: InsertResult = Customer::insert(customer_kate)
let res = Customer::insert(customer_kate)
.exec(db)
.await
.expect("could not insert customer");

View File

@ -8,7 +8,7 @@ pub async fn test_update_cake(db: &DbConn) {
profit_margin: Set(10.4),
..Default::default()
};
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
let bakery_insert_res = Bakery::insert(seaside_bakery)
.exec(db)
.await
.expect("could not insert bakery");
@ -22,7 +22,7 @@ pub async fn test_update_cake(db: &DbConn) {
..Default::default()
};
let cake_insert_res: InsertResult = Cake::insert(mud_cake)
let cake_insert_res = Cake::insert(mud_cake)
.exec(db)
.await
.expect("could not insert cake");
@ -36,7 +36,7 @@ pub async fn test_update_cake(db: &DbConn) {
let cake_model = cake.unwrap();
assert_eq!(cake_model.name, "Mud Cake");
assert_eq!(cake_model.price, dec!(10.25));
assert_eq!(cake_model.gluten_free, false);
assert!(!cake_model.gluten_free);
let mut cake_am: cake::ActiveModel = cake_model.into();
cake_am.name = Set("Extra chocolate mud cake".to_owned());
@ -62,7 +62,7 @@ pub async fn test_update_bakery(db: &DbConn) {
profit_margin: Set(10.4),
..Default::default()
};
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
let bakery_insert_res = Bakery::insert(seaside_bakery)
.exec(db)
.await
.expect("could not insert bakery");
@ -75,7 +75,7 @@ pub async fn test_update_bakery(db: &DbConn) {
assert!(bakery.is_some());
let bakery_model = bakery.unwrap();
assert_eq!(bakery_model.name, "SeaSide Bakery");
assert_eq!(bakery_model.profit_margin, 10.4);
assert!((bakery_model.profit_margin - 10.40).abs() < f64::EPSILON);
let mut bakery_am: bakery::ActiveModel = bakery_model.into();
bakery_am.name = Set("SeaBreeze Bakery".to_owned());
@ -92,7 +92,7 @@ pub async fn test_update_bakery(db: &DbConn) {
.expect("could not find bakery");
let bakery_model = bakery.unwrap();
assert_eq!(bakery_model.name, "SeaBreeze Bakery");
assert_eq!(bakery_model.profit_margin, 12.00);
assert!((bakery_model.profit_margin - 12.00).abs() < f64::EPSILON);
}
pub async fn test_update_deleted_customer(db: &DbConn) {
@ -130,8 +130,7 @@ pub async fn test_update_deleted_customer(db: &DbConn) {
assert_eq!(Customer::find().count(db).await.unwrap(), init_n_customers);
let customer: Option<customer::Model> =
Customer::find_by_id(customer_id.clone().unwrap() as i64)
let customer: Option<customer::Model> = Customer::find_by_id(customer_id.clone().unwrap())
.one(db)
.await
.expect("could not find customer");

View File

@ -1,10 +1,10 @@
use sea_orm::DatabaseConnection;
pub mod common;
pub use common::{bakery_chain::*, setup::*, TestContext};
mod crud;
pub use common::{bakery_chain::*, setup::*, TestContext};
pub use crud::*;
use sea_orm::DatabaseConnection;
// Run the test locally:
// DATABASE_URL="mysql://root:root@localhost" cargo test --features sqlx-mysql,runtime-async-std --test crud_tests
// DATABASE_URL="postgres://root:root@localhost" cargo test --features sqlx-postgres,runtime-async-std --test crud_tests
@ -20,18 +20,18 @@ async fn main() {
ctx.delete().await;
}
async fn create_entities(db: &DatabaseConnection) {
crud::test_create_bakery(db).await;
crud::create_baker::test_create_baker(db).await;
crud::test_create_customer(db).await;
crud::create_cake::test_create_cake(db).await;
crud::create_lineitem::test_create_lineitem(db).await;
crud::create_order::test_create_order(db).await;
pub async fn create_entities(db: &DatabaseConnection) {
test_create_bakery(db).await;
test_create_baker(db).await;
test_create_customer(db).await;
test_create_cake(db).await;
test_create_lineitem(db).await;
test_create_order(db).await;
crud::updates::test_update_cake(db).await;
crud::updates::test_update_bakery(db).await;
crud::updates::test_update_deleted_customer(db).await;
test_update_cake(db).await;
test_update_bakery(db).await;
test_update_deleted_customer(db).await;
crud::deletes::test_delete_cake(db).await;
crud::deletes::test_delete_bakery(db).await;
test_delete_cake(db).await;
test_delete_bakery(db).await;
}

View File

@ -0,0 +1,42 @@
pub mod common;
pub use common::{bakery_chain::*, setup::*, TestContext};
use sea_orm::{entity::prelude::*, DatabaseConnection, Set};
use uuid::Uuid;
#[sea_orm_macros::test]
#[cfg(any(
feature = "sqlx-mysql",
feature = "sqlx-sqlite",
feature = "sqlx-postgres"
))]
async fn main() -> Result<(), DbErr> {
let ctx = TestContext::new("bakery_chain_schema_primary_key_tests").await;
create_metadata(&ctx.db).await?;
ctx.delete().await;
Ok(())
}
pub async fn create_metadata(db: &DatabaseConnection) -> Result<(), DbErr> {
let metadata = metadata::ActiveModel {
uuid: Set(Uuid::new_v4()),
key: Set("markup".to_owned()),
value: Set("1.18".to_owned()),
};
let res = Metadata::insert(metadata.clone()).exec(db).await?;
assert_eq!(
res.last_insert_id,
if cfg!(feature = "sqlx-postgres") {
metadata.uuid.unwrap()
} else {
Default::default()
}
);
Ok(())
}

View File

@ -1,8 +1,8 @@
use sea_orm::entity::*;
use sea_orm::QueryFilter;
pub mod common;
pub use common::{bakery_chain::*, setup::*, TestContext};
pub use sea_orm::entity::*;
pub use sea_orm::QueryFilter;
// Run the test locally:
// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test query_tests

View File

@ -1,13 +1,14 @@
use chrono::offset::Utc;
use rust_decimal::prelude::*;
use rust_decimal_macros::dec;
use sea_orm::{entity::*, query::*, FromQueryResult};
pub mod common;
pub use chrono::offset::Utc;
pub use common::{bakery_chain::*, setup::*, TestContext};
pub use rust_decimal::prelude::*;
pub use rust_decimal_macros::dec;
pub use sea_orm::{entity::*, query::*, DbErr, FromQueryResult};
pub use uuid::Uuid;
// Run the test locally:
// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test relational_tests
// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std-native-tls --test relational_tests
#[sea_orm_macros::test]
#[cfg(any(
feature = "sqlx-mysql",
@ -474,3 +475,240 @@ pub async fn having() {
ctx.delete().await;
}
#[sea_orm_macros::test]
#[cfg(any(
feature = "sqlx-mysql",
feature = "sqlx-sqlite",
feature = "sqlx-postgres"
))]
pub async fn linked() -> Result<(), DbErr> {
use common::bakery_chain::Order;
use sea_orm::{SelectA, SelectB};
let ctx = TestContext::new("test_linked").await;
// SeaSide Bakery
let seaside_bakery = bakery::ActiveModel {
name: Set("SeaSide Bakery".to_owned()),
profit_margin: Set(10.4),
..Default::default()
};
let seaside_bakery_res = Bakery::insert(seaside_bakery).exec(&ctx.db).await?;
// Bob's Baker, Cake & Cake Baker
let baker_bob = baker::ActiveModel {
name: Set("Baker Bob".to_owned()),
contact_details: Set(serde_json::json!({
"mobile": "+61424000000",
"home": "0395555555",
"address": "12 Test St, Testville, Vic, Australia"
})),
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
..Default::default()
};
let baker_bob_res = Baker::insert(baker_bob).exec(&ctx.db).await?;
let mud_cake = cake::ActiveModel {
name: Set("Mud Cake".to_owned()),
price: Set(dec!(10.25)),
gluten_free: Set(false),
serial: Set(Uuid::new_v4()),
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
..Default::default()
};
let mud_cake_res = Cake::insert(mud_cake).exec(&ctx.db).await?;
let bob_cakes_bakers = cakes_bakers::ActiveModel {
cake_id: Set(mud_cake_res.last_insert_id as i32),
baker_id: Set(baker_bob_res.last_insert_id as i32),
..Default::default()
};
CakesBakers::insert(bob_cakes_bakers).exec(&ctx.db).await?;
// Bobby's Baker, Cake & Cake Baker
let baker_bobby = baker::ActiveModel {
name: Set("Baker Bobby".to_owned()),
contact_details: Set(serde_json::json!({
"mobile": "+85212345678",
})),
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
..Default::default()
};
let baker_bobby_res = Baker::insert(baker_bobby).exec(&ctx.db).await?;
let cheese_cake = cake::ActiveModel {
name: Set("Cheese Cake".to_owned()),
price: Set(dec!(20.5)),
gluten_free: Set(false),
serial: Set(Uuid::new_v4()),
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
..Default::default()
};
let cheese_cake_res = Cake::insert(cheese_cake).exec(&ctx.db).await?;
let bobby_cakes_bakers = cakes_bakers::ActiveModel {
cake_id: Set(cheese_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?;
let chocolate_cake = cake::ActiveModel {
name: Set("Chocolate Cake".to_owned()),
price: Set(dec!(30.15)),
gluten_free: Set(false),
serial: Set(Uuid::new_v4()),
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
..Default::default()
};
let chocolate_cake_res = Cake::insert(chocolate_cake).exec(&ctx.db).await?;
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?;
// Kate's Customer, Order & Line Item
let customer_kate = customer::ActiveModel {
name: Set("Kate".to_owned()),
notes: Set(Some("Loves cheese cake".to_owned())),
..Default::default()
};
let customer_kate_res = Customer::insert(customer_kate).exec(&ctx.db).await?;
let kate_order_1 = order::ActiveModel {
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
customer_id: Set(customer_kate_res.last_insert_id as i32),
total: Set(dec!(15.10)),
placed_at: Set(Utc::now().naive_utc()),
..Default::default()
};
let kate_order_1_res = Order::insert(kate_order_1).exec(&ctx.db).await?;
lineitem::ActiveModel {
cake_id: Set(cheese_cake_res.last_insert_id as i32),
order_id: Set(kate_order_1_res.last_insert_id as i32),
price: Set(dec!(7.55)),
quantity: Set(2),
..Default::default()
}
.save(&ctx.db)
.await?;
let kate_order_2 = order::ActiveModel {
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
customer_id: Set(customer_kate_res.last_insert_id as i32),
total: Set(dec!(29.7)),
placed_at: Set(Utc::now().naive_utc()),
..Default::default()
};
let kate_order_2_res = Order::insert(kate_order_2).exec(&ctx.db).await?;
lineitem::ActiveModel {
cake_id: Set(chocolate_cake_res.last_insert_id as i32),
order_id: Set(kate_order_2_res.last_insert_id as i32),
price: Set(dec!(9.9)),
quantity: Set(3),
..Default::default()
}
.save(&ctx.db)
.await?;
// Kara's Customer, Order & Line Item
let customer_kara = customer::ActiveModel {
name: Set("Kara".to_owned()),
notes: Set(Some("Loves all cakes".to_owned())),
..Default::default()
};
let customer_kara_res = Customer::insert(customer_kara).exec(&ctx.db).await?;
let kara_order_1 = order::ActiveModel {
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
customer_id: Set(customer_kara_res.last_insert_id as i32),
total: Set(dec!(15.10)),
placed_at: Set(Utc::now().naive_utc()),
..Default::default()
};
let kara_order_1_res = Order::insert(kara_order_1).exec(&ctx.db).await?;
lineitem::ActiveModel {
cake_id: Set(mud_cake_res.last_insert_id as i32),
order_id: Set(kara_order_1_res.last_insert_id as i32),
price: Set(dec!(7.55)),
quantity: Set(2),
..Default::default()
}
.save(&ctx.db)
.await?;
let kara_order_2 = order::ActiveModel {
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
customer_id: Set(customer_kara_res.last_insert_id as i32),
total: Set(dec!(29.7)),
placed_at: Set(Utc::now().naive_utc()),
..Default::default()
};
let kara_order_2_res = Order::insert(kara_order_2).exec(&ctx.db).await?;
lineitem::ActiveModel {
cake_id: Set(cheese_cake_res.last_insert_id as i32),
order_id: Set(kara_order_2_res.last_insert_id as i32),
price: Set(dec!(9.9)),
quantity: Set(3),
..Default::default()
}
.save(&ctx.db)
.await?;
#[derive(Debug, FromQueryResult, PartialEq)]
struct BakerLite {
name: String,
}
#[derive(Debug, FromQueryResult, PartialEq)]
struct CustomerLite {
name: String,
}
let baked_for_customers: Vec<(BakerLite, Option<CustomerLite>)> = Baker::find()
.find_also_linked(baker::BakedForCustomer)
.select_only()
.column_as(baker::Column::Name, (SelectA, baker::Column::Name))
.column_as(customer::Column::Name, (SelectB, customer::Column::Name))
.group_by(baker::Column::Id)
.group_by(customer::Column::Id)
.group_by(baker::Column::Name)
.group_by(customer::Column::Name)
.order_by_asc(baker::Column::Id)
.order_by_asc(customer::Column::Id)
.into_model()
.all(&ctx.db)
.await?;
assert_eq!(
baked_for_customers,
vec![
(
BakerLite {
name: "Baker Bob".to_owned(),
},
Some(CustomerLite {
name: "Kara".to_owned(),
})
),
(
BakerLite {
name: "Baker Bobby".to_owned(),
},
Some(CustomerLite {
name: "Kate".to_owned(),
})
),
(
BakerLite {
name: "Baker Bobby".to_owned(),
},
Some(CustomerLite {
name: "Kara".to_owned(),
})
),
]
);
ctx.delete().await;
Ok(())
}

View File

@ -1,11 +1,11 @@
use chrono::offset::Utc;
use rust_decimal::prelude::*;
use rust_decimal_macros::dec;
use sea_orm::{entity::*, query::*, DatabaseConnection, FromQueryResult};
use uuid::Uuid;
pub mod common;
pub use chrono::offset::Utc;
pub use common::{bakery_chain::*, setup::*, TestContext};
pub use rust_decimal::prelude::*;
pub use rust_decimal_macros::dec;
pub use sea_orm::{entity::*, query::*, DatabaseConnection, FromQueryResult};
pub use uuid::Uuid;
// Run the test locally:
// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test sequential_op_tests
@ -67,7 +67,7 @@ async fn init_setup(db: &DatabaseConnection) {
..Default::default()
};
let cake_insert_res: InsertResult = Cake::insert(mud_cake)
let cake_insert_res = Cake::insert(mud_cake)
.exec(db)
.await
.expect("could not insert cake");
@ -78,10 +78,18 @@ async fn init_setup(db: &DatabaseConnection) {
..Default::default()
};
let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker)
let cake_baker_res = CakesBakers::insert(cake_baker.clone())
.exec(db)
.await
.expect("could not insert cake_baker");
assert_eq!(
cake_baker_res.last_insert_id,
if cfg!(feature = "sqlx-postgres") {
(cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap())
} else {
Default::default()
}
);
let customer_kate = customer::ActiveModel {
name: Set("Kate".to_owned()),
@ -183,7 +191,7 @@ async fn find_baker_least_sales(db: &DatabaseConnection) -> Option<baker::Model>
results.sort_by(|a, b| b.cakes_sold.cmp(&a.cakes_sold));
Baker::find_by_id(results.last().unwrap().id as i64)
Baker::find_by_id(results.last().unwrap().id)
.one(db)
.await
.unwrap()
@ -200,7 +208,7 @@ async fn create_cake(db: &DatabaseConnection, baker: baker::Model) -> Option<cak
..Default::default()
};
let cake_insert_res: InsertResult = Cake::insert(new_cake)
let cake_insert_res = Cake::insert(new_cake)
.exec(db)
.await
.expect("could not insert cake");
@ -211,10 +219,18 @@ async fn create_cake(db: &DatabaseConnection, baker: baker::Model) -> Option<cak
..Default::default()
};
let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker)
let cake_baker_res = CakesBakers::insert(cake_baker.clone())
.exec(db)
.await
.expect("could not insert cake_baker");
assert_eq!(
cake_baker_res.last_insert_id,
if cfg!(feature = "sqlx-postgres") {
(cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap())
} else {
Default::default()
}
);
Cake::find_by_id(cake_insert_res.last_insert_id)
.one(db)